😎

Viteで作成したReact+TypeScriptプロジェクトにTailwindCSS+Storybookを導入する

2021/12/17に公開約16,300字

TL;DR

本記事では、Viteで作成したReact+TypeScriptプロジェクトに、TailwindCSSとStorybookを導入しています。とりあえず試したい人は、下のリポジトリを適当にクローンして使ってください。

https://github.com/Alesion30/viteapp-template

※ 上記のリポジトリでは、本記事では触れていないreact-router-domも導入しています。

今回使用している技術について

  • React v17.0.2
  • TypeScript v4.4.4
  • Vite v2.7.2
  • TailwindCSS v3.0.2
  • @storybook/react v6.4.9

Viteとは?

https://vitejs.dev/

Viteは、従来のビルドツール(react-scripts・VueCLI)に比べて、高速で動作するビルドツールです。Reactだけでなく、Vue・Svelte・Preactなどにも対応しています。create-react-appのように、簡単にテンプレートアプリを作成することができます。

$ yarn create vite (アプリ名) --template (プリセット名)

サポートしているプリセットは以下の通りです。(2021年12月17日現在)

JavaScript TypeScript
vanilla vanilla-ts
vue vue-ts
react react-ts
preact preact-ts
lit lit-ts
svelte svelte-ts

TailwindCSSとは?

https://tailwindcss.com/

TailwindCSSは、CSSフレームワークの一種で、classに直接bg-whitefont-boldなどを指定して、スタイルを適用していくのが特徴です。

下記のようなボタンを作りたいときは、以下のようにclassを指定します。

<button
  class="
    bg-orange-500
    hover:bg-orange-600
    text-white
    px-4
    py-2
    rounded-lg
    shadow-lg
  "
>
  ボタン
</button>

Storybookとは?

https://storybook.js.org/

storybookは、UIコンポーネントを一覧で表示できるドキュメント生成ツールです。生成したコンポーネントに対してstoryを記述するだけで、簡単にドキュメント上でコンポーネントの見た目を確認できます。また、ドキュメント上でpropsの制御も可能になっており、リアルタイムでpropsに応じたUIを表示することができます。

プロジェクトの作成(本題)

動作環境

nodeとyarnはインストール済みとします。

$ node -v
v16.0.0
$ yarn -v
1.22.11

React+TypeScriptの雛形アプリを作成

$ yarn create vite viteapp --template react-ts
$ cd viteapp
$ yarn
$ yarn dev

ESlint・Prettierの導入

ESlintとは、JavaScript・TypeScriptのコードの誤りがないかチェックしてくれるツールです。
Prettierとは、JavaScript・TypeScriptはもちろんのこと、JSONやHTMLなどさまざまなコードをいい感じにコード整形(フォーマット)してくれるツールです。

https://eslint.org/

https://prettier.io/

また、今回は複数のnpmスクリプトを一つのコマンドで実行させるために、npm-run-allも導入しています。

https://www.npmjs.com/package/npm-run-all
$ yarn add -D prettier eslint eslint-config-prettier eslint-plugin-{import,prettier,react,react-hooks}
$ yarn add -D @typescript-eslint/{parser,eslint-plugin}
$ yarn add -D npm-run-all
$ touch .eslintrc .prettierrc .eslintignore .prettierignore

下記は、自分がReactのプロジェクトでいつも使っているeslintとprettierの設定になります。

.eslintrc
{
  "root": true,
  "env": {
    "es6": true,
    "browser": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:import/typescript",
    "plugin:react/recommended",
    "plugin:prettier/recommended"
  ],
  "plugins": ["@typescript-eslint", "import", "react", "react-hooks"],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": ["tsconfig.json"],
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "settings": {
    "react": {
      "version": "detect"
    }
  },
  "rules": {
    // Possible Errors
    "no-unexpected-multiline": "error",
    // Best Practices
    "class-methods-use-this": "off",
    "consistent-return": [
      "error",
      {
        "treatUndefinedAsUnspecified": true
      }
    ],
    "dot-location": ["error", "property"],
    "no-implicit-globals": "error",
    "no-invalid-this": "error",
    "no-param-reassign": [
      "error",
      {
        "props": false
      }
    ],
    "no-unmodified-loop-condition": "error",
    "no-useless-call": "error",
    "no-void": "off",
    "no-else-return": "off",
    "no-catch-shadow": "error",
    "no-label-var": "error",
    "no-shadow": "off",
    "no-undef-init": "error",
    "no-unused-expressions": [
      "error",
      {
        "allowShortCircuit": true
      }
    ],
    "no-unused-vars": 1,
    "no-undef": "off",
    "no-empty": "off",
    "sort-imports": 0,
    "import/order": [
      "error",
      {
        "groups": ["builtin", "external", "parent", "sibling", "index", "object", "type"],
        "pathGroups": [
          {
            "pattern": "@alias/**",
            "group": "parent",
            "position": "before"
          }
        ],
        "alphabetize": {
          "order": "asc"
        },
        "newlines-between": "always"
      }
    ],
    // ES2015
    "constructor-super": "error",
    "generator-star-spacing": ["error", "after"],
    "no-this-before-super": "error",
    "prefer-arrow-callback": [
      "error",
      {
        "allowNamedFunctions": true
      }
    ],
    "prefer-spread": "error",
    "prefer-template": "off",
    // React
    "react/no-danger": "error",
    "react/no-deprecated": "error",
    "react/no-did-mount-set-state": "error",
    "react/no-did-update-set-state": "error",
    "react/no-direct-mutation-state": "error",
    "react/no-is-mounted": "error",
    "react/no-set-state": "error",
    "react/no-string-refs": "error",
    "react/prefer-stateless-function": "error",
    "react/prop-types": "off",
    "react/self-closing-comp": "off",
    "react/destructuring-assignment": "off",
    "@typescript-eslint/no-unused-vars": "off",
    "react-hooks/rules-of-hooks": "error",
    "react/react-in-jsx-scope": "off"
  }
}
.prettierrc
{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "useTabs": false,
  "jsxSingleQuote": true
}
.eslintignore
node_modules
dist
dist-ssr
.prettierignore
node_modules
dist
dist-ssr

vite.config.tsにて、eslintのエラーが出るので、tsconfig.jsonのincludeに./vite.config.tsを追加します。

tsconfig.json
{
- "include": ["./src"]
+ "include": ["./src", "./vite.config.ts"]
}

npmスクリプトに、ESlint・Prettierの実行コードを追加します。

package.json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
+   "fix": "npm-run-all -p fix:*",
+   "fix:eslint": "eslint . --ext .js,.ts,.jsx,.tsx --fix",
+   "fix:prettier": "prettier --write .",
+   "eslint": "eslint . --ext .js,.ts,.jsx,.tsx"
  },
}
# eslintの実行
$ yarn eslint
# eslintの実行(fixモード)
$ yarn fix:eslint
# prettierの実行
$ yarn fix:prettier
# コードの修正(fix:eslintとfix:prettierを実行)
$ yarn fix

Husky・lint-stagedを導入

Huskyとは、コミット時やプッシュ時などに何らかの処理を実行できるようにするツールです。
lint-stagedとは、ステージングしているファイルに対して何らかの処理を実行するようにできるツールです。

https://typicode.github.io/husky/#/

https://www.npmjs.com/package/lint-staged

今回はこれらを合わせて、コミット時に、ESlintとPrettierを実行するようにします。

$ yarn add -D husky lint-staged
$ npx husky-init && yarn install

npx husky-initを実行すると、.husky/pre-commitファイルが自動生成されます。

package.jsonにlint-stagedの設定を追加します。下記は、ステージングされているファイルがjs・ts・jsx・tsxの場合は、eslint --fix && prettier --writeを実行し、json・html・cssの場合は、prettier --writeを実行している例です。

package.json
{
+  "lint-staged": {
+    "*.{js,ts,jsx,tsx}": [ 
+      "eslint --fix",
+      "prettier --write"
+    ],
+    "*.{json,html,css}": [ 
+      "prettier --write"
+    ]
+  },
}

.husky/pre-commitを以下のように修正して、コミット時にlint-stagedを実行するようにします。

.husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

+ npx lint-staged

Visual Studio Codeの設定

chromeでデバッグできるように設定します。F5キーを押すと、chromeが立ち上がり、vscode上でブレークポイントなどを設定できるようになります。

$ touch .vscode/launch.json
.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "pwa-chrome",
      "request": "launch",
      "name": "Launch Chrome",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}"
    }
  ]
}

ファイルの表示・非表示の制御もここで行います。files.excludeでフォルダ・ファイルを指定すると、vscode上では、そのフォルダは非表示になります。node_modulesとかを閲覧するケースはほとんどないと思いますので、非表示にした方が良いです。(※個人的な意見です)

$ touch .vscode/settings.json
.vscode/settings.json
{
  "css.lint.unknownAtRules": "ignore",
  "files.exclude": {
    "node_modules": true,
    "dist": true,
    "yarn.lock": true,
  }
}

EditorConfigも設定しておきます。

$ touch .editorconfig
.editorconfig
root = true

[*]

indent_style = space
indent_size = 2

end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

パスエイリアスの設定

相対パスだとimport文が冗長になるので、src以下は~でimportできるようにします。

例) index.tsxからbuttonを読み込む場合

src/
|- components/
|  └─ button.tsx
|- pages/
|  └─ xxx/
|     └─ index.tsx
- import Button from '../../components/button';
+ import Button from '~/components/button';

nodeの型定義ファイルが必要になるので、インストールします。

$ yarn add -D @types/node
vite.config.ts
+ import { resolve } from 'path'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
+ resolve: {
+   alias: {
+     '~': resolve(__dirname, 'src'),
+   },
+ },
})
tsconfig.json
{
  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "~/*": ["src/*"]   
+    }
  }
}

TailwindCSSを導入

$ yarn add -D tailwindcss postcss autoprefixer
$ yarn tailwindcss init -p

yarn tailwindcss init -pを実行すると、postcss.config.jstailwind.config.jsが生成されます。ただ、module.exportsを使用しているので、eslintの対象から外しておきます。

.eslintignore
node_modules
dist
dist-ssr
+ postcss.config.js
+ tailwind.config.js

TailwindCSSのスタイルを読み込みを行うために、import 'tailwindcss/tailwind.css'を追加します。

src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom'

import '~/index.css'
+ import 'tailwindcss/tailwind.css'
import App from '~/App'

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

ビルドサイズを縮小するために、purgeの設定もしておきます。

tailwind.config.js
module.exports = {
- content: [],
+ content: ['index.html', 'src/**/*.{ts,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
}

既存のApp.tsxをTailwindCSSによるスタイリングでリファクタリングします。

App.tsx
import { useState } from 'react'
import logo from '~/logo.svg'

const App: React.VFC = () => {
  const [count, setCount] = useState(0)

  return (
    <div className='text-center'>
      <header className='bg-slate-700 min-h-screen flex flex-col align-center justify-center text-3xl text-white'>
        <img src={logo} className='h-72' alt='logo' />
        <p className='text-4xl'>Hello Vite + React!</p>
        <p className='my-5'>
          count is:
          <button
            type='button'
            className='bg-gray-50 hover:bg-gray-100 text-black p-2 mx-2'
            onClick={() => setCount((count) => count + 1)}
          >
            {count}
          </button>
        </p>
        <p className='my-2'>
          Edit <code>App.tsx</code> and save to test HMR updates.
        </p>
        <p className='my-2'>
          <a className='text-cyan-300' href='https://reactjs.org' target='_blank' rel='noopener noreferrer'>
            Learn React
          </a>
          {' | '}
          <a
            className='text-cyan-300'
            href='https://vitejs.dev/guide/features.html'
            target='_blank'
            rel='noopener noreferrer'
          >
            Vite Docs
          </a>
        </p>
      </header>
    </div>
  )
}

export default App

Storybookを導入

$ npx sb init --builder webpack5
$ yarn add -D webpack@^5
$ yarn add -D @types/babel__core
発生したエラー
node_modules/@vitejs/plugin-react/dist/index.d.ts:1:36 - error TS7016: Could not find a declaration file for module '@babel/core'. '***/viteapp/node_modules/@vitejs/plugin-react/node_modules/@babel/core/lib/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/babel__core` if it exists or add a new declaration (.d.ts) file containing `declare module '@babel/core';`

1 import type { ParserOptions } from '@babel/core';

npx sb init --builder webpack5を実行すると、storybookに必要なファイルがいくつか作成され、npmスクリプトにstorybookbuild-storybookが追加されます。

いくつかstoryファイルのサンプルも追加されていますが、src/stories/Introduction.stories.mdxsrc/stories/assets以外は削除します。

以下のコマンドで、Storybookが立ち上がります。

$ yarn storybook

http://localhost:6006/ にアクセスすると、以下の画面が表示されます。

storybook側でも同様にエイリアスの設定をします。

.storybook/main.js
+ const path = require('path')

module.exports = {
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
+ webpackFinal: async (config) => {
+   config.resolve.alias = {
+     ...config.resolve.alias,
+     '~': path.resolve(__dirname, '../src'),
+   }
+   return config
+ },
  framework: '@storybook/react',
  core: {
    builder: 'webpack5',
  },
}

TailwindCSSに対応させるためのアドオンも導入します。

$ yarn add -D @storybook/addon-postcss
.storybook/main.js
module.exports = {
+ addons: [
+   {
+     name: '@storybook/addon-postcss',
+     options: {
+       postcssLoaderOptions: {
+         implementation: require('postcss'),
+       },
+     },
+   },
+ ],
  webpackFinal: async (config) => {
    config.resolve.alias = {
      ...config.resolve.alias,
      '~': path.resolve(__dirname, '../src'),
    }
    return config
  },
  framework: '@storybook/react',
  core: {
    builder: 'webpack5',
  },
}
.storybook/preview.js
+ import '../src/index.css'
+ import 'tailwindcss/tailwind.css'

export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
}

ボタンコンポーネントを実装

App.tsxのボタンをコンポーネント化し、Storybook上で確認していきます。

コンポーネントを実装する時に、classNameを結合するためのライブラリclsxを使用しているので、こちらをインストールしておきます。

https://www.npmjs.com/package/clsx
$ touch src/components/button.tsx
src/components/button.tsx
import clsx from 'clsx'

export type ButtonProps = {
  children: React.ReactChild
  onClick?: () => void
  className?: string
  full?: boolean
  rounded?: boolean
  outlined?: boolean
}

export const Button: React.VFC<ButtonProps> = ({
  children,
  onClick,
  className,
  full = false,
  rounded = false,
  outlined = false,
}) => {
  return (
    <button
      onClick={onClick}
      type='button'
      className={clsx(
        className,
        'px-4 py-2 border shadow-sm text-base font-medium',
        full ? 'w-full' : '',
        rounded ? 'rounded-full' : 'rounded-md',
        outlined
          ? 'border-orange-600 text-orange-600 bg-white hover:bg-orange-50'
          : 'border-transparent text-white bg-orange-600 hover:bg-orange-700'
      )}
    >
      {children}
    </button>
  )
}

App.tsxのボタンを先ほど作成したボタンに置き換えます。

src/App.tsx
+ import { Button } from '~/components/button'

// JSX内のボタンを先ほど作成したコンポーネントにそのまま置き換えてください
- <button
-  type='button'
-  className='bg-gray-50 hover:bg-gray-100 text-black p-2 mx-2'
-  onClick={() => setCount((count) => count + 1)}
- >
-   {count}
- </button>

+ <Button outlined onClick={() => setCount((count) => count + 1)}>
+   {count}
+ </Button>

先ほど作成したボタンコンポーネントをStorybook上で表示するために、storyファイルを作成します。

$ touch src/stories/components/button.stories.tsx
src/stories/components/button.stories.tsx
import { ComponentMeta, ComponentStory } from '@storybook/react'

import { Button, ButtonProps } from '~/components/button'

export default {
  title: 'Components/Button',
  component: Button,
} as ComponentMeta<typeof Button>

const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />
const defaultArgs: ButtonProps = {
  children: 'ボタン',
  full: false,
  rounded: false,
  outlined: false,
}

export const Default = Template.bind({})
Default.storyName = 'ボタン'
Default.args = defaultArgs

Storybook上で、ボタンコンポーネントのUIを確認できるようになりました🎉

終わりに

本記事では、個人的に激推ししている構成を紹介しています。Viteのビルド速度には今年一番感動しました。今後は、このプロジェクトをベースにRecoilやAmplifyなどを導入した記事を書いていきたいと思います。

https://recoiljs.org/

https://aws.amazon.com/jp/amplify/

PS. 初めてZennに投稿した記事なので、拙い文章で読みずらかったかと思います、、。普段は、ReactやFlutterを使ってひそびそと開発を楽しんでいるただの学生なので、今後も気が向いたときに、ReactやFlutterについて記事を投稿していきたいなと思っています😎

Discussion

ログインするとコメントできます