🐉

2024年 React 環境構築 with Vite

2024/01/04に公開
2

はじめに

書いていて気づいたのですが、この記事に特に目新しいものはありません。コピペで最速環境構築をしたい方向けです。それぞれのツールについて細かい解説はしていないため、詳細は公式ドキュメントをご参照ください。

リポジトリはこちら。

https://github.com/kazukixmatsuda/react-setup-2024

Node.js

https://volta.sh/

この記事では Node.js のバージョン管理に volta を使用しますが、nvmnodebrew などでも問題ありません。パッケージマネージャーには pnpm を使用したいところですが、2024 年 1 月現在、volta の pnpm サポートは実験段階のため、今回は npm を使用します。(そこまでして volta を使用したい理由はないのですが...)

curl https://get.volta.sh | bash
source ~/.zshrc # or ~/.bashrc
volta install node # LTS版をインストール
node -v

Vite

https://ja.vitejs.dev/

フロントエンドエコシステム全体が Vite に寄っている (筆者の主観) + 早い + opinionated なため設定が簡単、という理由から Vite を使用します。コンパイラには Babel ではなく SWC を使用したいため、react-swc-ts テンプレートを使用します。

npm create vite@latest my-react-app -- --template react-swc-ts
cd my-react-app
npm install
npm run dev # 開発サーバーを起動
npm run build # プロダクションビルド
npm run preview # プロダクションビルドしたものをローカルで確認

Node.js、npm のバージョンをプロジェクトで固定したい場合。

volta pin node
volta pin npm

下記コードが追加されます。

package.json
"volta": {
  "node": "20.10.0",
  "npm": "10.2.5"
}

続いてパスエイリアスの設定です。import Header from "../../../../components/Header" のように書くのは面倒なので、import Header from "@/components/Header" と書けるようにします。tsconfig.json を編集します。既存の設定に baseUrlpaths を追加します。

tsconfig.json
"compilerOptions": {
  "baseUrl": "./",
  "paths": {
    "@/*": ["src/*"]
  }
}

本来、パスエイリアスを設定するには tsconfig.jsonvite.config.ts の両方を編集する必要がありますが、vite-tsconfig-paths を使用することで tsconfig.json のみで設定できるようになります。

npm install -D vite-tsconfig-paths

vite.config.ts を編集。

vite.config.ts
import react from "@vitejs/plugin-react-swc";
import { defineConfig } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
});

Vitest

https://vitest.dev/

続いてテスト周りの設定です。TypeScript などの設定が簡単であり、Jest 互換の使い慣れた API であるため Vitest を使用します。

npm install -D \
  vitest \
  happy-dom \
  @vitest/coverage-v8 \
  @testing-library/react \
  @testing-library/user-event \
  @testing-library/jest-dom \
  msw
  • vitest: Vitest の本体。
  • happy-dom: GUI を持たないブラウザ JavaScript の実装であり、JSDOM よりも高速。
  • @vitest/coverage-v8: テストカバレッジを計測するために使用。
  • @testing-library/react: React コンポーネントをテストするために使用。
  • @testing-library/user-event: テストでユーザーの操作をシミュレートするために使用。
  • @testing-library/jest-dom: DOM の状態をアサートするカスタムマッチャー。Vitest でも使用可能。toBeDisabledtoBeInTheDocument などが使用できるようになる。
  • msw: API モックを作成するために使用。

最初にサンプル用のコンポーネントを作成します。(本来はエラー処理などが必要ですが省略しています)

src/components/Sample.tsx
import { useState } from "react";

type User = {
  firstName: string;
  lastName: string;
};

const Sample = () => {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  const handleClick = async () => {
    setIsLoading(true);
    const res = await fetch("https://api.example.com/user");
    const user = await res.json();
    setUser(user);
    setIsLoading(false);
  };

  if (isLoading) {
    return <div>Loading...</div>;
  }

  const username = user ? `${user.firstName} ${user.lastName}` : "Anonymous";

  return (
    <div>
      <h1>Hello, {username}</h1>
      <button onClick={handleClick}>ユーザー名取得</button>
    </div>
  );
};

export default Sample;

続いて API モックを作成します。MSW のリポジトリにある example のコードをそのまま使用します。

mocks/handlers.ts
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("https://api.example.com/user", () => {
    return HttpResponse.json({
      firstName: "John",
      lastName: "Maverick",
    });
  }),
];
mocks/node.ts
import { setupServer } from "msw/node";

import { handlers } from "./handlers";

export const server = setupServer(...handlers);

package.json を編集。

package.json
{
  "scripts": {
    "test": "vitest run --coverage",
    "update-snapshots": "vitest run --update"
  }
}

vite.config.ts を編集。Vitest の設定は test プロパティに記載。Vite と Vitest で設定が共有できのるが楽です。筆者は expect 関数などをインポートしたい派なので、globals はデフォルトの false のままにしています。

vite.config.ts
/// <reference types="vitest" />
import react from "@vitejs/plugin-react-swc";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    // globals: true,
    environment: "happy-dom",
    setupFiles: ["./vitest-setup.ts"],
    // スナップショットの保存先を設定
    resolveSnapshotPath: (path, extension) => {
      return path.replace("/src/", "/__snapshots__/") + extension;
    },
  },
});

vitest-setup.ts を作成。

vitest-setup.ts
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { beforeAll, afterEach, afterAll } from "vitest";

import { server } from "./mocks/node.js";

beforeAll(() => {
  server.listen();
});

afterEach(() => {
  server.resetHandlers();
  // cleanup は vite.config.ts の test.globals を true にした場合は不要
  cleanup();
});

afterAll(() => {
  server.close();
});

tsconfig.json を編集。includevitest-setup.ts を追加。vite.config.tsglobalstrue にした場合は、typesvitest/globals を追加。

tsconfig.json
{
  "compilerOptions": {
    "types": ["vitest/globals"]
  },
  "include": ["src", "vitest-setup.ts"],
}

テストコードを書きます。

src/components/Sample.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, test } from "vitest";

import Sample from "./Sample";

test("サンプルテスト", async () => {
  const user = userEvent.setup()
  render(<Sample />);
  expect(screen.getByRole("heading")).toHaveTextContent("Hello, Anonymous");
  const button = screen.getByRole("button", { name: "ユーザー名取得" });
  user.click(button);
  expect(await screen.findByText("Loading...")).toBeInTheDocument();
  expect(await screen.findByRole("heading")).toHaveTextContent(
    "Hello, John Maverick",
  );
});

test("スナップショットテスト", () => {
  const { container } = render(<Sample />);
  expect(container).toMatchSnapshot();
});

テスト実行 ( coverage ディレクトリが作成されるので .gitignore へ追加をお勧めします )

npm run test

カバレッジの扱い方ですが、プルリクエストに表示するようにしたり、カバレッジが閾値を下回った場合に CI が失敗するようにしたり、そこはチームの方針で決めてください。

ESLint + Prettier

https://eslint.org/

https://prettier.io/

Biome などを使用してみたいですが、まだ筆者が試せていないため、この記事では ESLint + Prettier を使用します。deno fmt + deno lint も良いかもしれません。

Vite はプロジェクト作成時に ESLint の設定もしてくれるため、TypeScript の対応などは不要です。ここでは追加の設定を行います。

npm install -D \
  eslint-plugin-react \
  eslint-plugin-vitest \
  eslint-plugin-import \
  prettier \
  eslint-config-prettier \
  npm-run-all

eslintrc.cjs を編集。

.eslintrc.cjs
/** @type {import('eslint/lib/shared/types').ConfigData} */
module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended", // 追加
    "plugin:react/jsx-runtime",  // 追加
    "plugin:react-hooks/recommended",
    "plugin:vitest/recommended", // 追加
    "prettier", // 追加
  ],
  ignorePatterns: ["dist", ".eslintrc.cjs"],
  parser: "@typescript-eslint/parser",
  plugins: [
    "react-refresh",
    "import" // 追加
  ],
  settings: {
    react: { version: "detect" }, // 追加
  },
  rules: {
    "react-refresh/only-export-components": [
      "warn",
      { allowConstantExport: true },
    ],
    // it ではなく test の使用を強制
    "vitest/consistent-test-it": ["error", { fn: "test" }],
    // import の並び順を設定
    "import/order": [
      "warn",
      {
        groups: [
          "builtin",
          "external",
          "internal",
          ["parent", "sibling"],
          "object",
          "type",
          "index",
        ],
        "newlines-between": "always",
        pathGroupsExcludedImportTypes: ["builtin"],
        alphabetize: { order: "asc", caseInsensitive: true },
        pathGroups: [
          {
            pattern: "react",
            group: "external",
            position: "before",
          },
        ],
      },
    ],
  },
};

(宣伝) もし ESLint の設定ファイルの書き方が分からない場合は、以前記事を書いたので参考にしてください。

https://zenn.dev/kazukix/articles/eslint-config-react-native

エディターなどのツールに Prettier を使用していることを知らせるため prettier.config.js を作成します。筆者は Prettier のデフォルトルールが好みなので、追加の設定は加えていません。チームのルールに合わせて設定を変更してください。

prettier.config.js
/** @type {import("prettier").Config} */
const config = {};

export default config;

package.json を編集。run-prun-snpm-run-all のコマンドです。

package.json
{
  "scripts": {
    "lint": "run-p lint:*",
    "lint:tsc": "tsc --noEmit",
    "lint:prettier": "prettier . --check",
    "lint:eslint": "eslint . --ext .ts,.tsx",
    "fix": "run-s fix:*",
    "fix:prettier": "prettier . --write",
    "fix:eslint": "eslint . --ext .ts,.tsx --fix",
  }
}

実行できるか確認。

npm run fix
npm run lint

Husky + lint-staged

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

https://github.com/lint-staged/lint-staged

コミットする前に ESLint と Prettier を実行するために Husky と lint-staged を使用します。

git init
npm install --save-dev husky lint-staged
npx husky init
echo "npx lint-staged" > .husky/pre-commit

package.json を編集。

package.json
{
  "lint-staged": {
    "*.{ts,tsx}": "eslint --fix",
    "*": "prettier --write --ignore-unknown"
  }
}

動作確認。

git add .
git commit -m "init"

Storybook

https://storybook.js.org/

Story ファイルの書き方などは割愛します。下記コマンドでサンプルが src/stories に作成されるので、そちらを参考にしてください。

npx storybook@latest init

Storybook を使用するからにはローカルだけではなく、どこかにデプロイしてチームメンバー全員で共有したいです。その方法も Storybook の公式サイトに記載があります。この記事では、公式サイトと同じように Chromatic にデプロイします。Chromatic とは Storybook のメンテナーが作成した、無料のホスティングサービスです。Storybook は静的サイトとしてビルドできるので、GitHub Pages などにデプロイすることも可能です。

Chromatic をインストール。

npm install -D chromatic

Chromatic にログインして、Github リポジトリと連携します。連携が完了後、下記コマンドを実行すると Storybook がデプロイされます。project-token は Chromatic の画面に表示されています。

npx chromatic --project-token=<project-token>

ただ、毎回手動でデプロイするのは手間なので、Github Actions のワークフローを作成します。secrets の登録が必要なため注意です。詳しい手順はこちら

.github/workflows/chromatic.yml
name: Chromatic Deployment

on:
  push:
    branches:
      - main

jobs:
  chromatic:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - run: npm ci
      - uses: chromaui/action@latest
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

main ブランチに push すると自動でデプロイされます。

その他

下記項目は、好みやプロジェクトの要件によって変わるので、この記事では書きませんが筆者の好みを一応乗せておきます。

  • 状態管理
    • Jotai が好き。とはいえ実務で使った経験はないので、2024 年は挑戦したい。
    • Recoil はメンテナンス面で少し心配。
    • Zustand は今後主流になりそう。
    • React Context は本当にシンプルなアプリケーションであれば使えると思う。ただ、どの道辛くなる可能性が高く、ボイラープレートが多めなのも微妙。
    • (宣伝) 1 年前の記事ですが、React の状態管理ライブラリに関する記事を書いたのでよければ参考にしてください。
  • ルーティング
    • 好みというより、React Router しか使ったことがない。
  • データフェッチ
    • REST であれば TanStack Query
    • GraphQL であれば Apollo Client。( TanStack Query は GraphQL にも対応している )
    • 標準の Fetch API のみで十分な場合もある。
  • スタイリング
    • vanilla-extract が使いやすい。型がつく CSS Modules。ファイル名などの制約が厳しめなのも好み。Stylelint がサポートされれば完璧。
    • shadcn/ui も少し触ってみた感じ良さそう。
    • 先日 Meta が発表した stylex は気になっている。
    • Tailwind CSS は好みではないがエコシステムが充実しているのは良いと思う。
    • styled-components は慣れているが、React コンポーネントなのか、styled-components がスタイルを当てたタグなのかが分かりにくい、という点は微妙だと思う。書き方にもよるが、コンポーネントファイルが肥大化しやすいのも好みではない。界隈全体的にゼロランタイムな CSS in JS が主流になっていくと予想しており、新規プロジェクトでの採用は悩ましいところ。
  • フォーム
  • E2E
    • Playwright が使いやすい。とはいえノーコードツールにも注目したい。

CI

Lint とテストを実行する Github Actions のワークフローを作成します。

.github/workflows/lint.yml
name: Lint

on: push

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npm run lint
.github/workflows/test.yml
name: Test

on: push

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npm run test

CD

Vite で作成したアプリケーションのデプロイ方法ですが、公式サイトにて様々な方法が紹介されているためこの記事では割愛します。具体的には下記のようなサービスがデプロイ先として考えられます。

  • GitHub Pages
  • GitLab Pages
  • Netlify
  • Vercel
  • Cloudflare Pages
  • Firebase Hosting
  • AWS Amplify Hosting

公式サイトには記載がないですが、AWS であれば CloudFront × S3、Google Cloud であれば Cloud CDN × Cloud Storage という組み合わせもあります。

GitHubで編集を提案

Discussion

michiharumichiharu

vite-tsconfig-paths と vanilla-extract は知りませんでした!どちらも便利そうですね。使ってみたいと思いっます!

りんだりんだ

環境構築こちらでしました。
初学者なので勉強になりました。

僕の環境ではeslint 9になっていて、一部使えなかったものがあったので、メモになります。
eslintの --ext フラグですが、なくても動くようになっていました。
eslint.config.js側で、対象ファイルを指定することができるようになっています。

そのため、package.json内の--extフラグを消す必要があります。

また、eslint-plugin-importが eslint 9に対応していないので、importできません。