Viteで作成したReact+TypeScriptプロジェクトにTailwindCSS+Storybookを導入する
TL;DR
本記事では、Viteで作成したReact+TypeScriptプロジェクトに、TailwindCSSとStorybookを導入しています。とりあえず試したい人は、下のリポジトリを適当にクローンして使ってください。
※ 上記のリポジトリでは、本記事では触れていない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とは?
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とは?
TailwindCSSは、CSSフレームワークの一種で、classに直接bg-white
やfont-bold
などを指定して、スタイルを適用していくのが特徴です。
下記のようなボタンを作りたいときは、以下のようにclassを指定します。
<button
class="
bg-orange-500
hover:bg-orange-600
text-white
px-4
py-2
rounded-lg
shadow-lg
"
>
ボタン
</button>
Storybookとは?
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などさまざまなコードをいい感じにコード整形(フォーマット)してくれるツールです。
また、今回は複数のnpmスクリプトを1つのコマンドで実行させるために、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の設定になります。
{
"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"
}
}
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false,
"jsxSingleQuote": true
}
node_modules
dist
dist-ssr
node_modules
dist
dist-ssr
vite.config.tsにて、eslintのエラーが出るので、tsconfig.jsonのincludeに./vite.config.ts
を追加します。
{
- "include": ["./src"]
+ "include": ["./src", "./vite.config.ts"]
}
npmスクリプトに、ESlint・Prettierの実行コードを追加します。
{
"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とは、ステージングしているファイルに対して何らかの処理を実行するようにできるツールです。
今回はこれらを合わせて、コミット時に、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
を実行している例です。
{
+ "lint-staged": {
+ "*.{js,ts,jsx,tsx}": [
+ "eslint --fix",
+ "prettier --write"
+ ],
+ "*.{json,html,css}": [
+ "prettier --write"
+ ]
+ },
}
.husky/pre-commitを以下のように修正して、コミット時にlint-stagedを実行するようにします。
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
+ npx lint-staged
Visual Studio Codeの設定
chromeでデバッグできるように設定します。F5キーを押すと、chromeが立ち上がり、vscode上でブレークポイントなどを設定できるようになります。
$ touch .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
{
"css.lint.unknownAtRules": "ignore",
"files.exclude": {
"node_modules": true,
"dist": true,
"yarn.lock": true,
}
}
EditorConfigも設定しておきます。
$ touch .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
+ 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'),
+ },
+ },
})
{
"compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["src/*"]
+ }
}
}
TailwindCSSを導入
$ yarn add -D tailwindcss postcss autoprefixer
$ yarn tailwindcss init -p
yarn tailwindcss init -p
を実行すると、postcss.config.js
とtailwind.config.js
が生成されます。ただ、module.exportsを使用しているので、eslintの対象から外しておきます。
node_modules
dist
dist-ssr
+ postcss.config.js
+ tailwind.config.js
TailwindCSSのスタイルを読み込みを行うために、import 'tailwindcss/tailwind.css'
を追加します。
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の設定もしておきます。
module.exports = {
- content: [],
+ content: ['index.html', 'src/**/*.{ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
}
既存のApp.tsxをTailwindCSSによるスタイリングでリファクタリングします。
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スクリプトにstorybook
とbuild-storybook
が追加されます。
いくつかstoryファイルのサンプルも追加されていますが、src/stories/Introduction.stories.mdx
とsrc/stories/assets
以外は削除します。
以下のコマンドで、Storybookが立ち上がります。
$ yarn storybook
http://localhost:6006/ にアクセスすると、以下の画面が表示されます。
storybook側でも同様にエイリアスの設定をします。
+ 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
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',
},
}
+ 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
を使用しているので、こちらをインストールしておきます。
$ touch 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のボタンを先ほど作成したボタンに置き換えます。
+ 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
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などを導入した記事を書いていきたいと思います。
PS. 初めてZennに投稿した記事なので、拙い文章で読みずらかったかと思います、、。普段は、ReactやFlutterを使ってひそびそと開発を楽しんでいるただの学生なので、今後も気が向いたときに、ReactやFlutterについて記事を投稿していきたいなと思っています😎
Discussion