ViteでReact + TypeScript + TailwindCSSの環境構築をする
(2022年11月25日追記)
私の本が株式会社インプレス R&Dさんより出版されました。この記事の内容も含まれています。イラストは鍋料理さんの作品です。猫のモデルはなんとうちのコです!
感想を書いていただけるととても嬉しいです!
(2022年8月3日追記)この記事の内容はこちらの本でも読めます。無料公開している「1章 フロントエンドの環境構築」に相当します。本の方がより詳しく説明しています。
(2022年5月24日追記)React 18に対応しました。
はじめに
私はサーバーサイドエンジニアですが、フロントエンドの技術力も強化しようと思いReactを勉強中です。現在は勉強を兼ねてとあるAPIを販売するWEBサービスを作ろうとしています。技術選定がようやく終わり、開発環境構築手順が固まってきたので、手順書にまとめました。人気のある技術を中心に選んだので、適用範囲の広い組み合わせになっていると思います。
技術選定
この記事では以下の技術を取り上げています。
- React
- TypeScript
- Vite
- ESLint
- Prettier
- TailwindCSS
基本的には「りあクト! TypeScriptで始めるつらくないReact開発 第3.1版」の構成を参考にしていますが、変えているところもいくつかあります。
Create React Appの代わりにViteを使う
一番悩んだのがこちらです。create-react-app
はReact公式のビルドツールで、ViteはVue.jsを開発したEvan You氏が開発したビルドツールです。できることはほぼ同じで、実行速度はViteの方が圧倒的に速いので、最近Viteの人気が高まっています。
npm trendsを見ると3倍の差をつけてViteの圧勝ですが、技術選定をしていた昨年の秋頃はまだここまで差はついていなかったのでかなり悩みました。でも開発時の待ち時間は少ないほど良いので、Viteを選択してよかったと思います。
TailwindCSSを使う
個人的にCSSは自分では書きたくないと思っているので、何らかのCSSフレームワークやUIライブラリを使うことは確定していましたが、何を使うかはかなり迷いました。Chakra UIやMUI(旧Material UIなども検討しましたが、一番人気があってカスタマイズの自由度が高いTailwindCSSに決めました。
また、CSSを書かないことに決めたので、Stylelintはあえて導入しませんでした。
環境構築手順
環境構築済みのリポジトリはこちらです。
前提条件は以下の通りです。
- Node.jsのLTS推奨版がインストールされていること(執筆時は16.13.0で作業)
- yarnではなくnpmを使用する
- エディターはVSCodeを使用する
Viteで新規プロジェクトを生成する(React + TypeScript)
Viteは標準でReactとTypeScriptに対応しています。下記のようにコマンドを実行して、対話形式で設定していくと新規プロジェクトが生成されます。
npm create vite@latest
✔ Project name: … {任意のプロジェクト名}
✔ Select a framework: › react
✔ Select a variant: › react-ts
いったん動かしてみましょう。
cd {プロジェクト名}
npm install
npm run dev
ブラウザでlocalhost:3000にアクセスして下図のように表示されればとりあえずOKです。
ESLintの設定
個人的にフロントエンド開発で一番重要でかつ難しいのがESLintの設定だと思います。内容を理解せずに使っている人も結構多いのではないでしょうか?今回のプロジェクトではESLintの理解に一番時間がかかりました。
まずはインストールします。
npm install -D eslint
次に初期設定します。対話形式で多くの質問に答える必要があります。
npm init @eslint/config
How would you like to use ESLint?:
To check syntax, find problems, and enforce code style
What type of modules does your project use?:
JavaScript modules (import/export)
Which framework does your project use?: React
Does your project use TypeScript?: Yes
Where does your code run?: Browser
How would you like to define a style for your project?:
Use a popular style guide
Which style guide do you want to follow?:
Airbnb: https://github.com/airbnb/javascript
What format do you want your config file to be in?:
JavaScript
Checking peerDependencies of eslint-config-airbnb@latest
The config that you've selected requires the following dependencies:
eslint-plugin-react@^7.28.0 @typescript-eslint/eslint-plugin@latest eslint-config-airbnb@latest eslint@^7.32.0 || ^8.2.0 eslint-plugin-import@^2.25.3 eslint-plugin-jsx-a11y@^6.5.1 eslint-plugin-react-hooks@^4.3.0 @typescript-eslint/parser@latest
Would you like to install them now with npm?: Yes
yarnを使っている場合は最後の質問にNoと答えて手動でモジュールをインストールする必要があります。これが地味に面倒くさいので今回のプロジェクトではnpmを使っています。
上記の通り質問に正しく答えると、以下の.eslintrc.js
ファイルが生成されます。
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'plugin:react/recommended',
'airbnb',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: [
'react',
'@typescript-eslint',
],
rules: {
},
};
ESLintはさまざまなコーディングルールを追加のプラグインとしてインストールできます。しかしプラグインをインストールしただけではルールが有効にならないため、.eslintrc.js
に記述を追加する必要があります。必要なルールを適用するため、以下のように書き換えましょう。
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'plugin:react/recommended',
'airbnb',
+ 'airbnb/hooks',
+ 'plugin:import/errors',
+ 'plugin:import/warnings',
+ 'plugin:import/typescript',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:@typescript-eslint/recommended-requiring-type-checking',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
+ project: './tsconfig.eslint.json',
sourceType: 'module',
+ tsconfigRootDir: __dirname,
},
plugins: [
+ 'import',
+ 'jsx-a11y',
'react',
+ 'react-hooks',
'@typescript-eslint'
],
+ root: true,
rules: {
+ 'no-use-before-define': 'off',
+ '@typescript-eslint/no-use-before-define': ['error'],
+ 'lines-between-class-members': [
+ 'error',
+ 'always',
+ {
+ exceptAfterSingleLine: true,
+ },
+ ],
+ 'no-void': [
+ 'error',
+ {
+ allowAsStatement: true,
+ },
+ ],
+ 'padding-line-between-statements': [
+ 'error',
+ {
+ blankLine: 'always',
+ prev: '*',
+ next: 'return',
+ },
+ ],
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ {
+ vars: 'all',
+ args: 'after-used',
+ argsIgnorePattern: '_',
+ ignoreRestSiblings: false,
+ varsIgnorePattern: '_',
+ },
+ ],
+ 'import/extensions': [
+ 'error',
+ 'ignorePackages',
+ {
+ js: 'never',
+ jsx: 'never',
+ ts: 'never',
+ tsx: 'never',
+ },
+ ],
+ 'react/jsx-filename-extension': [
+ 'error',
+ {
+ extensions: ['.jsx', '.tsx'],
+ },
+ ],
+ 'react/jsx-props-no-spreading': [
+ 'error',
+ {
+ html: 'enforce',
+ custom: 'enforce',
+ explicitSpread: 'ignore',
+ },
+ ],
+ 'react/react-in-jsx-scope': 'off',
+ },
+ overrides: [
+ {
+ files: ['*.tsx'],
+ rules: {
+ 'react/prop-types': 'off',
+ },
+ },
+ ],
+ settings: {
+ 'import/resolver': {
+ node: {
+ paths: ['src'],
+ },
+ },
},
};
急に長くなって大変ですが、ひとつずつ確認していきましょう。まずは.eslintrc.js
の構文について表にまとめました。
.eslintrc.jsの設定項目 | 説明 |
---|---|
extends | 各プラグインルールの推奨の共有設定をプラグイン開発者が提供しているので、ここで指定する。 順番が重要。 競合する設定は後に記述されたものによって上書きされる。 |
parserOptions | ESLintのパーサへ渡すオプションを設定する。 |
parserOptions.project | プロジェクトのTypeScriptコンパイル設定ファイルのパスをパーサに教えるための設定。tsconfig.jsonではなくtsconfig.eslint.jsonという別ファイルを用意して渡している。こうしないとパーサがローカルにインストールされたnpmパッケージのファイルまでパースしてしまう。 |
parserOptions.tsconfigRootDir | 相対パスの起点。 |
plugins | 読み込ませる追加ルールのプラグインを設定する。ここに記述しないとプラグインは有効にならないので要注意。 |
root | ESLintはデフォルトの挙動として親ディレクトリの設定ファイルまで読み込んでしまう。trueにすることでその挙動を抑制している。 |
rules | 各ルールの適用の可否やエラーレベルを設定する。主にextendsで読み込んだ共有設定を書き換える場合に用いる。 |
overrides | 任意のglobパターンにマッチするファイルのみ、ルールの適用を上書きする。ここでは*.tsxファイルに対してのみreact/prop-typesを無効化するのに利用している。 |
settings | 任意の実行ルールに適用される追加の共有設定。詳しくは後述。 |
settingsで解決している問題の発生メカニズムと解消方法は以下の通りです。こんなの設定ファイルを見ただけでは絶対わからないので、ドキュメントを残すのはとても大事です。
- 前提として
tsconfig.json
でsrc/
配下のファイルを絶対パスでインポートできるようにしている - このままでは
eslint-plugin-import
がその絶対パスを解決できずにエラーとなる -
eslint-plugin-import
は内部でeslint-import-resolver-node
というモジュール解決プラグインを使用している -
eslint-import-resolver-node
に対して、そのパスにsrcを追加することでエラーを解消している
上の表に出てくるtsconfig.eslint.json
も追加しましょう。
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules"
]
}
追加・変更したルールの説明はこちらです。内容はほぼ「りあクト!」からの引用です。
ルール | 説明 |
---|---|
no-use-before-define, @typescript-eslint/no-use-before-define | 定義前の変数の使用を禁じる ESLint と TypeScript ESLint のルール。このルールは適用すべきか微妙なところで、実は最終形のeslintrcでは外している。 |
ines-between-class-members | クラスメンバーの定義の間に空行を入れるかどうかを定義するルール。1行記述のメンバーのときは空行を入れなくていいように緩めている。 |
no-void | void 演算子の(式としての)使用を禁ずるルール。Effect Hook内で非同期処理を記述する際、@typescript-eslint/no-floating-promisesルールに抵触するのを回避するのにvoid文を記述する必要があるため、文としての使用のみを許可している。 |
padding-line-between-statements | 任意の構文の間に区切りの空行を入れるかどうかを定義するルール。ここではreturn文の前に常に空行を入れるよう設定している。 |
@typescript-eslint/no-unused-vars | 使用していない変数の定義を許さないルール。ここでは変数名を「_」にしたときのみ許容するように設定。 |
mport/extensions | インポートの際のファイル拡張子を記述するかを定義するルール。npmパッケージ以外のファイルについて .js、.jsx、.ts、.tsx のファイルのみ拡張子を省略し、他のファイルは拡張子を記述させるように設定。 |
react/jsx-filename-extension | JSXのファイル拡張子を制限するルール。eslint-config-airbnbで.jsxのみに限定されているので、.tsxを追加。 |
react/jsx-props-no-spreading | JSXでコンポーネントを呼ぶときのpropsの記述にスプレッド構文を許さないルール。eslint-config-airbnbにてすべて禁止されているが、<Foo {...{ bar, baz } /}> のように個々のpropsを明記する書き方のみ許容するように設定。 |
react/react-in-jsx-scope | JSX記述を使用する場合にreactモジュールをReactとしてインポートすることを強制する。新しいJSX変換形式を用いる場合はインポートが不要になるためこの設定を無効化。 |
react/prop-types | コンポーネントのpropsに型チェックを行うためのpropTypesプロパティの定義を強制するルール。eslint-config-airbnbで設定されているが、TypeScriptの場合は不要なのでファイル拡張子が.tsxの場合に無効化するよう設定を上書き。 |
余計なファイルがESLintの対象にならないように.eslintignore
も追加しましょう。
build/
public/
**/coverage/
**/node_modules/
**/*.min.js
*.config.js
.*lintrc.js
さらに関数定義をアロー関数式に統一するためのルールを追加適用します。まずはプラグインのイントールから。
npm install -D eslint-plugin-prefer-arrow
.eslintrc.js
のpluginsとrulesを下記のように修正します。
plugins: [
'import',
'jsx-a11y',
+ 'prefer-arrow',
'react',
'react-hooks',
'@typescript-eslint',
],
+ 'prefer-arrow/prefer-arrow-functions': [
+ 'error',
+ {
+ disallowPrototype: true,
+ singleReturnOnly: false,
+ classPropertiesAllowed: false,
+ },
+ ],
これだけだとreact/function-component-definition
と競合してしまったので、rulesにさらに以下の記述を追加します。
+ 'react/function-component-definition': [
+ 'error',
+ {
+ namedComponents: 'arrow-function',
+ unnamedComponents: 'arrow-function',
+ },
+ ],
ESLint単体の設定はこれで完了です。
Prettierの設定
PrettierはESLintと比べたら設定項目が少なくてとても楽です。まずはインストールから。
npm install -D prettier eslint-config-prettier
次に.eslintrc.js
のextends
にprettier用の記述を追加します。上でも述べた通りextends
は順番が非常に重要です。prettierの記述は一番最後に来るように設定しましょう。
extends: [
'plugin:react/recommended',
'airbnb',
'airbnb/hooks',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
+ 'prettier',
],
次に.prettierrc
を追加します。ESLintの設定に比べたらかわいいものですね。
{
"singleQuote": true,
"trailingComma": "all",
"endOfLine": "auto"
}
.eslintrc.jsの最終形
ここまでで.eslintrc.js
の修正が終わったので全体を載せておきます。
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'plugin:react/recommended',
'airbnb',
'airbnb/hooks',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 'latest',
project: './tsconfig.eslint.json',
sourceType: 'module',
tsconfigRootDir: __dirname,
},
plugins: [
'import',
'jsx-a11y',
'prefer-arrow',
'react',
'react-hooks',
'@typescript-eslint'
],
root: true,
rules: {
'lines-between-class-members': [
'error',
'always',
{
exceptAfterSingleLine: true,
},
],
'no-void': [
'error',
{
allowAsStatement: true,
},
],
'padding-line-between-statements': [
'error',
{
blankLine: 'always',
prev: '*',
next: 'return',
},
],
'prefer-arrow/prefer-arrow-functions': [
'error',
{
disallowPrototype: true,
singleReturnOnly: false,
classPropertiesAllowed: false,
},
],
'react/function-component-definition': [
'error',
{
namedComponents: 'arrow-function',
unnamedComponents: 'arrow-function',
},
],
'@typescript-eslint/no-unused-vars': [
'error',
{
vars: 'all',
args: 'after-used',
argsIgnorePattern: '_',
ignoreRestSiblings: false,
varsIgnorePattern: '_',
},
],
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'react/jsx-filename-extension': [
'error',
{
extensions: ['.jsx', '.tsx'],
},
],
'react/jsx-props-no-spreading': [
'error',
{
html: 'enforce',
custom: 'enforce',
explicitSpread: 'ignore',
},
],
'react/react-in-jsx-scope': 'off',
},
overrides: [
{
files: ['*.tsx'],
rules: {
'react/prop-types': 'off',
},
},
],
settings: {
'import/resolver': {
node: {
paths: ['src'],
},
},
},
};
VSCode用の設定
ファイルを保存したときに自動でlintとフォーマットが走るようにしたいので、.vscode/settings.json
を追加します。
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": false,
"eslint.packageManager": "npm",
"typescript.enablePromptUseWorkspaceTsdk": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[graphql]": {
"editor.formatOnSave": true
},
"[javascript]": {
"editor.formatOnSave": true
},
"[javascriptreact]": {
"editor.formatOnSave": true
},
"[json]": {
"editor.formatOnSave": true
},
"[typescript]": {
"editor.formatOnSave": true
},
"[typescriptreact]": {
"editor.formatOnSave": true
},
}
ここまでの設定が正しくできていれば、App.tsx
を下記のように修正したときにエラーが表示されないはずです。またファイル保存時に自動的に整形されるのが確認できると思います。
import { useState, VFC } from 'react';
import logo from './logo.svg';
import './App.css';
const App: VFC = () => {
const [count, setCount] = useState(0);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Hello Vite + React!</p>
<p>
<button type="button" onClick={() => setCount((c) => c + 1)}>
count is: {count}
</button>
</p>
<p>
Edit <code>App.tsx</code> and save to test HMR updates.
</p>
<p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
{' | '}
<a
className="App-link"
href="https://vitejs.dev/guide/features.html"
target="_blank"
rel="noopener noreferrer"
>
Vite Docs
</a>
</p>
</header>
</div>
);
};
export default App;
コミット前にlintを自動実行する設定
念の為、コミット前にlintが自動で実行されるようにしておきましょう。まずsimple-git-hooks
とlint-staged
をインストールします。
npm install -D simple-git-hooks lint-staged
package.json
のscript
を以下のように修正します。
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
+ "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
+ "lint:fix": "eslint --fix 'src/**/*.{js,jsx,ts,tsx}'",
+ "lint:conflict": "eslint-config-prettier 'src/**/*.{js,jsx,ts,tsx}'",
+ "preinstall": "typesync || :",
+ "prepare": "simple-git-hooks > /dev/null"
},
ついでにlintを手動実行する設定と、npm install時にtypesyncを自動実行する設定も追加しています。
さらにpackage.json
の最後に以下の記述を追加しましょう。
+ "simple-git-hooks": {
+ "pre-commit": "npx lint-staged"
+ },
+ "lint-staged": {
+ "src/**/*.{js,jsx,ts,tsx}": [
+ "prettier --write --loglevel=error",
+ "eslint --fix --quiet"
+ ],
+ "{public,src}/**/*.{html,gql,graphql,json}": [
+ "prettier --write --loglevel=error"
+ ]
+ }
TailwindCSSの設定
まずはインストールから。
npm install -D tailwindcss postcss autoprefixer
次に初期設定を呼び出します。
npx tailwindcss init -p
これによりtailwind.config.js
とpostcss.config.js
が生成されます。tailwind.config.js
を以下のように書き換えましょう。
module.exports = {
content: ['./index.html', './src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
src/index.css
を下記の内容で上書きします。
@tailwind base;
@tailwind components;
@tailwind utilities;
ここで動作確認してみましょう。TailwindCSSはclassName
にあらかじめ決められたクラス名を入力することでデザインが適用されます。App.tsx
の一部を下記のように書き換えてみてください。
- <p>Hello Vite + React!</p>
+ <p className="text-blue-300 bg-red-600">Hello Vite + React!</p>
表示が下図のようになっていればTailwindCSSは適用されています。
className
の中身を自動でソートするプラグインも入れておきましょう。
npm install -D prettier-plugin-tailwindcss
このプラグインは設定不要です。VSCodeを再起動してApp.tsxを保存すれば、さきほど修正した箇所が下記のように整形されるはずです。
<p className="bg-red-600 text-blue-300">Hello Vite + React!</p>
まとめ
長くなりましたが以上となります。ESLintはもっと楽にならないかなー、とは思いますが、コーディングルールをプロジェクトごとに細かく制御したい気持ちもわかるので、今のところはがんばって設定を書いていくしかないのでしょう。せめて設定に書いてある内容は理解した上で使っていきたいと思います。
今はReact Routerを入れてモックを作っていますが、TailwindCSSで画面を構築していくのはかなり楽しいですね。
Discussion