🚀

Reactの開発環境を改めて作ってみた

2023/02/12に公開

Reactの開発環境構築

毎回Reactの開発環境構築時にいろんなサイトを彷徨って、結局毎回異なる環境になってしまうため、改めて環境構築をまとめてみました。
構築する環境は以下の通りです。

  • VSCode(RemoteContainer)
  • React(TypeScript)
  • Vite
  • Prettier
  • ESLint(Airbnb)
  • lint-staged
  • Husky
  • Jest

Node.jsの環境作成

RemoteContainerの説明は割愛します。
devcontainerの設定を追加していきます。

devcontainer.jsonの例

よく使う拡張機能を盛り込んでいます。

.devcontainer/devcontainer.json
{
  "name": "React Typescript Starter",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:0-18",
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "christian-kohler.path-intellisense",
        "mhutchie.git-graph",
        "VisualStudioExptTeam.vscodeintellicode",
        "formulahendry.auto-rename-tag",
        "formulahendry.auto-close-tag"
      ]
    }
  }
}

準備ができたらRemoteContainerの機能を使ってコンテナを立ち上げます。

ViteでReactのプロジェクトを作成

Vite公式でコマンドを確認。
私自身がyarn愛好家なので今回はyarnで実施。npmやpnpm派の方は置き換えて実施してください。

yarn create vite

Project name → 任意のプロジェクト名
Select a framework → React
Select a variant → TypeScript or TypeScript + SWC

プロジェクトの作成が完了したら、ルートディレクトリにProject nameで入力した名でフォルダが作成されます。
今回はルートディレクトリにプロジェクトを配置したいので、フォルダ内のファイル/フォルダをルートに移動します。

この時点では依存パッケージがインストールされていないので、yarn installでインストールする。node_modulesがルートディレクトリに作成されれば準備完了。

念のため、開発サーバーを立ち上げてページが確認できることを確認しましょう。

yarn dev

Prettier追加

以下のコマンドを実行して、prettierを追加します。

yarn add -D prettier

フォーマットの設定を行いたい場合は、.prettierrcを作成し設定を追加してください。
https://prettier.io/docs/en/configuration.html

VSCodeで保存時に自動でフォーマットをかけてほしいので、vscodeの設定を変更します。
今回はdevcontainer.jsonに追記していきます。

.devcontainer/devcontainer.json
{
  "name": "React Typescript Starter",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:0-18",
  "customizations": {
    "vscode": {
+      "settings": {
+        "editor.defaultFormatter": "esbenp.prettier-vscode",
+        "editor.formatOnSave": true
+      },
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "christian-kohler.path-intellisense",
        "mhutchie.git-graph",
        "VisualStudioExptTeam.vscodeintellicode",
        "formulahendry.auto-rename-tag",
        "formulahendry.auto-close-tag"
      ]
    }
  }
}

コンテナのリビルドが必要なので、リビルドを行います。
リビルドが完了したら、任意のファイルを開きPrettierでフォーマットされるか確認します。

ESLint(Airbnb)追加

ちょっとした開発には制限が厳しいかもしれませんが、TypeScriptの正しい使い方など勉強にもなるので、制約厳しめのAirbnbのlintを使用します。
以前まではESLintの初期化コマンドでAirbnbを選択できていたのですが、今はできないので一旦Standardの設定で初期化し、その後手動でAirbnbの設定を追加していきます。

ESLintの初期化

以下のコマンドを実行し、ESLintをインストールします。

yarn add -D eslint

次に以下のコマンドを実行し、ESLintの初期化を行います。

yarn eslint --init

CLIの入力

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? → Standard: ~
What format do you want your config file to be in? → JavaScript

ファイルのフォーマットはご自由に。(今回はJavaScriptを選択しています)
初期化が無事完了したら、ルートディレクトリに.eslintrc.cjsが作成されます。

Airbnbのスタイルガイドを追加する

初期で導入したStandardのスタイルガイドをアンインストールし、新たにAirbnbのスタイルガイドを導入します。
以下のコマンドを実行して、Standardのスタイルガイドをアンインストールします。

yarn remove eslint-config-standard-with-typescript

続けてAirbnbのスタイルガイドやその他諸々を導入していきます。

yarn add -D @typescript-eslint/parser eslint-config-{airbnb,airbnb-typescript,prettier} eslint-plugin-{react-hooks,jsx-a11y}
.eslintrc.cjs
module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: [
    "plugin:react/recommended",
+    "plugin:@typescript-eslint/recommended",
+    "airbnb",
+    "airbnb-typescript",
+    "airbnb/hooks",
+    "prettier",
-    "standard-with-typescript",
  ],
  overrides: [],
  parserOptions: {
    ecmaVersion: "latest",
-    sourceType: "module",
+    project: ["./tsconfig.json"],
  },
-  plugins: ["react"],
+  ignorePatterns: ["vite.config.ts", ".eslintrc.cjs"],
+  settings: {
+    react: {
+      version: "detect",
+    },
+  },
  rules: {
+    // Airbnbのルールから一部無効化
+    // Reactの明示的なインポートを不要にする(Reactv17で対応)
+    "react/react-in-jsx-scope": "off",
  },
};
.eslintrc.cjsの完成形
eslintrc.cjs
module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: [
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended",
    "airbnb",
    "airbnb-typescript",
    "airbnb/hooks",
    "prettier",
  ],
  overrides: [],
  parserOptions: {
    ecmaVersion: "latest",
    project: ["./tsconfig.json"],
  },
  ignorePatterns: ["vite.config.ts", ".eslintrc.cjs"],
  settings: {
    react: {
      version: "detect",
    },
  },
  rules: {
    // Airbnbのルールから一部無効化
    // Reactの明示的なインポートを不要にする(Reactv17で対応)
    "react/react-in-jsx-scope": "off",
  },
};

上記の設定で保存した後、App.tsxでエラーが発生すれば設定完了です。

Airbnbのガイドに沿って修正したApp.tsx
App.tsx
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import "./App.css";

function App() {
  const [count, setCount] = useState(0);

  return (
    <div className="App">
      <div>
        <a href="https://vitejs.dev" target="_blank" rel="noreferrer">
          <img src="/vite.svg" className="logo" alt="Vite logo" />
        </a>
        <a href="https://reactjs.org" target="_blank" rel="noreferrer">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button
          type="button"
          onClick={() => setCount((prevCount) => prevCount + 1)}
        >
          count is {count}
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </div>
  );
}

export default App;

importの自動ソートや不要なimport文を削除するように設定する

ファイルごとにimportの順が平仄あってなかったり、使ってないライブラリがあった場合整理したいなど思うことがあると思います。(少なくとも私は思います)
その問題を解決すべく、プラグインを導入していきます。

yarn add -D eslint-plugin-{import,unused-imports}

インストールできたら、.eslinttc.cjsを編集します。

.eslintrc.cjs
module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: [
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended",
    "airbnb",
    "airbnb-typescript",
    "airbnb/hooks",
    "prettier",
  ],
  overrides: [],
  parserOptions: {
    ecmaVersion: "latest",
    project: ["./tsconfig.json"],
  },
  ignorePatterns: ["vite.config.ts", ".eslintrc.cjs"],
  settings: {
    react: {
      version: "detect",
    },
  },
+  plugins: ["import", "unused-imports"],
  rules: {
    // Airbnbのルールから一部無効化
    // Reactの明示的なインポートを不要にする(Reactv17で対応)
    "react/react-in-jsx-scope": "off",

+  "import/order": [
+      // 条件に沿ったソート順になっていないと警告する
+      "warn",
+      {
+        groups: [
+          "builtin",
+          "external",
+          "parent",
+          "sibling",
+          "index",
+          "object",
+          "type",
+        ],
+        pathGroups: [
+          // React関係のimportは最上部にソートする
+          {
+            pattern: "{react,react-dom/**,react-router-dom}",
+            group: "builtin",
+            position: "before",
+          },
+        ],
+        alphabetize: {
+          order: "asc",
+        },
+        "newlines-between": "always",
+      },
+    ],
  }
};
.eslintrc.cjsの完成形
eslintrc.cjs
module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: [
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended",
    "airbnb",
    "airbnb-typescript",
    "airbnb/hooks",
    "prettier",
  ],
  overrides: [],
  parserOptions: {
    ecmaVersion: "latest",
    project: ["./tsconfig.json"],
  },
  ignorePatterns: ["vite.config.ts", ".eslintrc.cjs"],
  settings: {
    react: {
      version: "detect",
    },
  },
  plugins: ["import", "unused-imports"],
  rules: {
    // Airbnbのルールから一部無効化
    // Reactの明示的なインポートを不要にする(Reactv17で対応)
    "react/react-in-jsx-scope": "off",

    "import/order": [
      // 条件に沿ったソート順になっていないと警告する
      "warn",
      {
        groups: [
          "builtin",
          "external",
          "parent",
          "sibling",
          "index",
          "object",
          "type",
        ],
        pathGroups: [
          // React関係のimportは最上部にソートする
          {
            pattern: "{react,react-dom/**,react-router-dom}",
            group: "builtin",
            position: "before",
          },
        ],
        alphabetize: {
          order: "asc",
        },
        "newlines-between": "always",
      },
    ],
  },
};

保存後、App.tsxで警告が出ていれば正常に設定できています。

ただこのままでは、警告が出ているだけで手動で修正が必要になります。そんなことは手間なのでVSCodeに自動で行ってもらえるように設定しましょう。
devcontainer.jsonを以下の通りに編集してください。

.devcontainer/devcontainer.json
{
  "name": "React Typescript Starter",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:0-18",
  "customizations": {
    "vscode": {
      "settings": {
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "editor.formatOnSave": true,
+        "editor.codeActionsOnSave": {
+          "source.organizeImports": true,
+          "source.fixAll.eslint": true
+        }
      },
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "christian-kohler.path-intellisense",
        "mhutchie.git-graph",
        "VisualStudioExptTeam.vscodeintellicode",
        "formulahendry.auto-rename-tag",
        "formulahendry.auto-close-tag"
      ]
    }
  }
}
devcontainer.jsonの完成形
{
  "name": "React Typescript Starter",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:0-18",
  "customizations": {
    "vscode": {
      "settings": {
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "editor.formatOnSave": true,
        "editor.codeActionsOnSave": {
          "source.organizeImports": true,
          "source.fixAll.eslint": true
        }
      },
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "christian-kohler.path-intellisense",
        "mhutchie.git-graph",
        "VisualStudioExptTeam.vscodeintellicode",
        "formulahendry.auto-rename-tag",
        "formulahendry.auto-close-tag"
      ]
    }
  }
}

保存ができたら、コンテナの再ビルドを行ってください。
App.tsxを開いて保存をすると、Prettierのフォーマットおよびimportの整理が行われることを確認してください。

lint-staged + husky追加

パッケージのインストール

Gitにコミットするファイルはルールに従った状態のものをだけにしましょう。違反しているものをコミットするとESLintを導入している意味がありませんので。
ステージングしているファイルを確認することができるlint-stagedとコミット時の動作をカスタムできるHuskyを導入していきます。
以下のコマンドを実行して、必要なパッケージをインストールします。

yarn add -D husky lint-staged

lint-stagedの設定

package.jsonに設定を追加します。

package.json
"lint-staged": {
  "src/**/*.{js,ts,tsx}": [
    // 警告1件でもあればエラー
    "yarn eslint --max-warnings 0",
    "yarn prettier -w"
  ]
}

追記が完了したら、実際に動作するか確認します。あえてエラーやフォーマットされていない状態のApp.tsxを用意しました。

この状態でApp.tsxをステージングして、以下のコマンドを実行してみます。

yarn lint-staged


main.tsxも警告がありますが、ステージングされたApp.tsxだけが静的解析されていることがわかります。
では実際に問題点を修正して、prettierのフォーマットが行われるか確認します。(基本的には保存時にフォーマットされるように設定しているので、あまり意味はないですが…)

無事フォーマットされたことが確認できました。

huskyの設定

以下のコマンドを実行して、huskyの実行ファイルを用意します。

yarn husky install

コマンド実行後、ルートディレクトリに.huskyフォルダが作成されていれば初期セットアップは完了となります。
commit前にlint-stagedのコマンドが実行されるように設定を追加します。

yarn husky add .husky/pre-commit "yarn lint-staged"

コマンド実行が成功すると.husky/pre-commitファイルが作成されます。

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

yarn lint-staged

実際にコミット前にlint-stagedが実行されるか確認します。

実際にコミットを実行しようとすると、静的解析が始まりエラーが表示されコミットが中止されました。

husky installが適宜実行されるようにpackage.jsonにスクリプトを追加しておきます。

package.json
"scripts": {
  "dev": "vite",
  "build": "tsc && vite build",
  "preview": "vite preview",
+  "prepare": "husky install",    
},

Jest追加

パッケージ導入・セットアップ

必要なパッケージをインストールします。トランスパイラにts-jestを使うことが多いですが、コンパイルの時間がかかりがちなので最近ホット?な@swc/jestを使用します。
https://swc.rs/docs/usage/jest

yarn add -D jest @types/jest @swc/{core,jest} jest-environment-jsdom @testing-library/{jest-dom,react,react-hooks,user-event}

インストールが完了したら、jestの設定ファイルを作成します。

jest.config.cjs
module.exports = {
  roots: ["<rootDir>/test"],
  testMatch: ["**/?(*.)+(spec|test).+(js|jsx|ts|tsx)"],
  transform: {
    "^.+\\.(ts|tsx)$": [
      "@swc/jest",
      {
        sourceMaps: true,
        module: {
          type: "commonjs",
        },
        jsc: {
          parser: {
            syntax: "typescript",
            tsx: true,
          },
          transform: {
            react: {
              runtime: "automatic",
            },
          },
        },
      },
    ],
  },
  testEnvironment: "jest-environment-jsdom",
  setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
};

続けて公式に従って設定ファイルを作成します。
https://github.com/testing-library/jest-dom#usage

jest.setup.ts
import '@testing-library/jest-dom'

作成したファイルはESLintのチェック対象となっており、どちらともエラーが出るので一旦チェック対象外にします。

.eslintrc.cjs
ignorePatterns: [
  "vite.config.ts",
  ".eslintrc.cjs",
+  "jest.config.cjs",
+  "jest.setup.ts",
],

jest.setup.tstsconfig.jsonで読み込みます。

tsconfig.json
{
  "compilerOptions": {
   },
-  "include": ["src"],
+  "include": ["src", "jest.setup.ts"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

テストケース作成

この後App.tsxのテストケースを書いていきますが、テスト時に画像ファイルやCSSのインポートに失敗します。モックを準備する必要があるので、以下の記事に従ってモックを作成します。(@Amselさん、有益な情報をありがとうございます)
https://qiita.com/Amsel/items/8a4859d06a8de551abf8

test/App.test.tsx
import { render, screen } from "@testing-library/react";

import App from "../src/App";

test("Vite+ReactのWelcomeページが表示されている", () => {
  render(<App />);
  screen.debug();
  expect(screen.getByText("Vite + React")).toBeInTheDocument();
});

テスト実施

以下のコマンドを実行して、テストを実行します。

yarn jest --config ./jest.config.cjs


テストが成功したので、テスト設定は完了です。

package.jsonの更新

テストのためにコマンドをいちいちたたくのは面倒なので、スクリプトとして定義しておきます。

package.json
"scripts": {
  "dev": "vite",
  "build": "tsc && vite build",
  "preview": "vite preview",
  "prepare": "husky install",
+  "test": "jest --config ./jest.config.cjs"
+  "test:watch": "yarn test --watch"
},

おわりに

ここまでの内容をGitHubに公開しています。一から構築するのが面倒な方はこちらを使用していただければと思います。(スターをいただけると泣いて喜びます)
https://github.com/tHyt-lab/react-ts-starter

今回はテストライブラリにJestを使いましたが、VitestがProductionReadyになれば改めて再構築したいなと考えています。
様々なツールやライブラリがあって、モダンなフロント環境を追うのはなかなか大変ですがやっぱり楽しいですね。他にも便利なものは取り入れていってDX(DeveloperExperience)向上に努めていきたいです。

Discussion