🐧

ViteでReact + TypeScript + TailwindCSSの環境構築をする

2022/03/10に公開

(2022年11月25日追記)
私の本が株式会社インプレス R&Dさんより出版されました。この記事の内容も含まれています。イラストは鍋料理さんの作品です。猫のモデルはなんとうちのコです!

https://www.amazon.co.jp/dp/B0BMPZW444/

感想を書いていただけるととても嬉しいです!

(2022年8月3日追記)この記事の内容はこちらの本でも読めます。無料公開している「1章 フロントエンドの環境構築」に相当します。本の方がより詳しく説明しています。

https://zenn.dev/sikkim/books/how_to_create_api_sales_service

(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の人気が高まっています。

Vite vs Create React App

npm trendsを見ると3倍の差をつけてViteの圧勝ですが、技術選定をしていた昨年の秋頃はまだここまで差はついていなかったのでかなり悩みました。でも開発時の待ち時間は少ないほど良いので、Viteを選択してよかったと思います。

TailwindCSSを使う

個人的にCSSは自分では書きたくないと思っているので、何らかのCSSフレームワークやUIライブラリを使うことは確定していましたが、何を使うかはかなり迷いました。Chakra UIMUI(旧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です。

Hello Vite + React

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ファイルが生成されます。

.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に記述を追加する必要があります。必要なルールを適用するため、以下のように書き換えましょう。

.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.jsonsrc/配下のファイルを絶対パスでインポートできるようにしている
  • このままではeslint-plugin-importがその絶対パスを解決できずにエラーとなる
  • eslint-plugin-importは内部でeslint-import-resolver-nodeというモジュール解決プラグインを使用している
  • eslint-import-resolver-nodeに対して、そのパスにsrcを追加することでエラーを解消している

上の表に出てくるtsconfig.eslint.jsonも追加しましょう。

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も追加しましょう。

.eslintignore
build/
public/
**/coverage/
**/node_modules/
**/*.min.js
*.config.js
.*lintrc.js

さらに関数定義をアロー関数式に統一するためのルールを追加適用します。まずはプラグインのイントールから。

npm install -D eslint-plugin-prefer-arrow

.eslintrc.jsのpluginsとrulesを下記のように修正します。

.eslintrc.jsの一部(plugins)
plugins: [
    'import',
    'jsx-a11y',
+   'prefer-arrow',
    'react',
    'react-hooks',
    '@typescript-eslint',
  ],
.eslintrc.jsの一部(rules)
+   'prefer-arrow/prefer-arrow-functions': [
+     'error',
+     {
+       disallowPrototype: true,
+       singleReturnOnly: false,
+       classPropertiesAllowed: false,
+     },
+   ],

これだけだとreact/function-component-definitionと競合してしまったので、rulesにさらに以下の記述を追加します。

.eslintrc.jsの一部(rules)
+   'react/function-component-definition': [
+     'error',
+     {
+       namedComponents: 'arrow-function',
+       unnamedComponents: 'arrow-function',
+     },
+   ],

ESLint単体の設定はこれで完了です。

Prettierの設定

PrettierはESLintと比べたら設定項目が少なくてとても楽です。まずはインストールから。

npm install -D prettier eslint-config-prettier

次に.eslintrc.jsextendsにprettier用の記述を追加します。上でも述べた通りextendsは順番が非常に重要です。prettierの記述は一番最後に来るように設定しましょう。

.eslintrc.jsの一部(extends)
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の設定に比べたらかわいいものですね。

.prettierrc
{
  "singleQuote": true,
  "trailingComma": "all",
  "endOfLine": "auto"
}

.eslintrc.jsの最終形

ここまでで.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を追加します。

.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を下記のように修正したときにエラーが表示されないはずです。またファイル保存時に自動的に整形されるのが確認できると思います。

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-hookslint-stagedをインストールします。

npm install -D simple-git-hooks lint-staged

package.jsonscriptを以下のように修正します。

package.jsonの一部(scripts)
  "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の最後に以下の記述を追加しましょう。

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.jspostcss.config.jsが生成されます。tailwind.config.jsを以下のように書き換えましょう。

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

src/index.cssを下記の内容で上書きします。

src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;

ここで動作確認してみましょう。TailwindCSSはclassNameにあらかじめ決められたクラス名を入力することでデザインが適用されます。App.tsxの一部を下記のように書き換えてみてください。

App.tsx
-       <p>Hello Vite + React!</p>
+       <p className="text-blue-300 bg-red-600">Hello Vite + React!</p>

表示が下図のようになっていればTailwindCSSは適用されています。

TailwindCSSの動作確認

classNameの中身を自動でソートするプラグインも入れておきましょう。

npm install -D prettier-plugin-tailwindcss

このプラグインは設定不要です。VSCodeを再起動してApp.tsxを保存すれば、さきほど修正した箇所が下記のように整形されるはずです。

App.tsx
        <p className="bg-red-600 text-blue-300">Hello Vite + React!</p>

まとめ

長くなりましたが以上となります。ESLintはもっと楽にならないかなー、とは思いますが、コーディングルールをプロジェクトごとに細かく制御したい気持ちもわかるので、今のところはがんばって設定を書いていくしかないのでしょう。せめて設定に書いてある内容は理解した上で使っていきたいと思います。

今はReact Routerを入れてモックを作っていますが、TailwindCSSで画面を構築していくのはかなり楽しいですね。

Discussion