📯

React Router v7 セットアップ手順 - Biome, Vitest, shadcn/ui, Storybook

2025/01/27に公開

私の好きなフレームワークである Remix が React Router に統合されました。

🎉 Remix v2 → React Router v7

Rimix の最新版である react router v7 のセットアップ方法についてメモを残しておきます。

👇ソースはこちら

プロジェクトを作成

フレームワークとしてreact-routerを利用するためのコマンドを実行します。

プロジェクトを作成したい任意のフォルダにて
npx create-react-router@latest react-router-v7-boilerplate
git    Initialize a new git repository? Yes
deps   Install dependencies with npm? Yes

フォーマッター/リンター設定(Biome)

eslintとprettierは使わず、フォーマッターとリンターを兼ねているBiome(バイオーム)を利用します。

1. パッケージをインストール

npm install --save-dev --save-exact @biomejs/biome

2. 設定ファイルを生成

npx @biomejs/biome init

3. VSCode用の拡張機能を追加

①拡張機能をインストール

https://marketplace.visualstudio.com/items?itemName=biomejs.biome

.vscode/settings.jsonに設定を追加

.vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[javascript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[javascriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "editor.codeActionsOnSave": {
    "quickfix.biome": "always",
    "source.organizeImports.biome": "always"
  }
}

biome.jsonの設定を修正

biome.json
{
  "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true
  },
  "files": {
    "ignore": ["**/node_modules/**"]
  },
  "formatter": {
    "indentStyle": "space",
    "ignore": []
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "nursery": { "useSortedClasses": "warn" },
      "correctness": {
        "noUnusedImports": "warn",
        "noUnusedVariables": "warn"
      }
    }
  },
  "organizeImports": {
    "enabled": true
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single",
      "semicolons": "always"
    }
  },
  "overrides": [{
    "include": ["app/components/shadcn/ui/**"],
    "linter": {
      "rules": {
        "nursery": { "useSortedClasses": "off" }
      }
    }
  }]
}

4. コードチェック & 修正用のスクリプトを追加

package.json
  "scripts": {
    // ...
    "--- CHECK SECTION ---": "--- --- --- --- ---",
    "biome:check:write": "npx biome check --write",

VSCodeのデバッグ環境構築

VSCode上でフロントエンドとバックエンドを同時にデバッグ可能とするため、.vscode/launch.jsonを作成してデバッグ設定を追加します。

.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Backend",
      "type": "node-terminal",
      "request": "launch",
      "command": "npm run dev",
      "cwd": "${workspaceFolder}"
    },
    {
      "name": "Debug Frontend",
      "type": "chrome",
      "request": "launch",
      "url": "http://localhost:5173/",
      "webRoot": "${workspaceFolder}/app",
      "sourceMaps": true
    },
    {
      "name": "Debug Backend and Frontend",
      "type": "node-terminal",
      "request": "launch",
      "command": "npm run dev",
      "cwd": "${workspaceFolder}",
      "serverReadyAction": {
        "action": "startDebugging",
        "pattern": "Local: +https?://.+",
        "name": "Debug Frontend"
      }
    },
  ]
}

デバッグと実行で「Debug Backend and Frontend」を実行すれば、フロントエンドとバックエンド両方のブレークポイントが利用可能となります。

参考にした記事はこちらです。

vite.config.tsで環境変数を扱えるように変更

デプロイ時のPORT指定に備えて、PORTを環境変数で指定可能なようにしておきます。

1. .envを作成

.env
PORT=5173

2. .gitに.envが含まれないように設定

.gitignore
# ...既存の設定

# env
.env

.envをgitに含まないようにすると、必要な環境変数が分からなくなるため、.envと同じ構造でダミーの値を設定した.env.exampleを作成しておきます。

.env.example
# --- 開発時は、このファイルを `.env` ファイルに名称変更して利用すること --- #

PORT=5173

3. vite.config.tsを修正

.envの情報は、loadEnvから取得します。

vite.config.ts
import { reactRouter } from '@react-router/dev/vite';
import autoprefixer from 'autoprefixer';
import tailwindcss from 'tailwindcss';
import { defineConfig, loadEnv } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig(({ mode }) => {
  // Load env file based on `mode` in the current working directory.
  // Set the third parameter to '' to load all env regardless of the `VITE_` prefix.
  const env = loadEnv(mode, process.cwd(), '');

  return {
    css: {
      postcss: {
        plugins: [tailwindcss, autoprefixer],
      },
    },
    plugins: [reactRouter(), tsconfigPaths()],
    server: {
      port: Number.parseInt(env.PORT || '5173'),
    },
  };
});

Vitestを導入

1. パッケージをインストール

npm i -D vitest

2. vitestのglobal設定(テストファイルのimport省略)

vite.config.tsをベースにして、テスト関数のimportを省略する設定を加えます。
まずは、vitest.config.tsを作成します。

vitest.config.ts
import { type ConfigEnv, defineConfig, mergeConfig } from 'vitest/config';
import baseViteConfig from './vite.config';

export default defineConfig(async (configEnv: ConfigEnv) => {
  // NOTE: 環境変数の読み込み(loadEnv())が非同期的なためawaitを設定
  const baseConfig = await baseViteConfig(configEnv);

  return mergeConfig(
    baseConfig,
    defineConfig({
      test: {
        globals: true,
      },
    }),
  );
});

次に、tsconfig.jsonを修正します。

tsconfig.json
    "types": [
      "node",
      "vite/client",
+     "vitest/globals"
    ],

3. vitest動作確認用にテストファイルを追加

app/root.test.tsx
const sampleFunction = (a: number, b: number) => {
  return a + b;
};

test('adds 1 + 2 to equal 3', () => {
  expect(sampleFunction(1, 2)).toBe(3);
});

4. テスト実行用のスクリプトを追加

package.json
  "scripts": {
    // ...,
    "--- TEST SECTION ---": "--- --- --- --- ---",
    "test": "vitest"
  },

npm run testでテストが正常に動作すればOKです。

GitHubアクション設定

プルリク時に、コードチェック・ビルド・テストが通るか確認するためのCI設定を追加しておきます。

まず、CI用のコマンドを追加します。

package.json
  "scripts": {
    // ...
    "--- CI SECTION ---": "--- --- --- --- ---",
    "ci:check": "npx biome ci",
    "ci:build": "npx env-cmd -f .env.example npm run build",
    "ci:test": "npm run test"
  }

次に、mainブランチにプルリクした際のアクションを追加します。

.github/workflows/on-pr.yml
name: CI
on:
  push:
    branches:
      - main
  pull_request:

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - run: npm ci # 正確なバージョンのパッケージをインストール

      - run: npm run ci:check
      - run: npm run ci:build
      - run: npm run ci:test

ファイルベースルーティング設定

Remix v2のファイルベースルーティングを利用するように設定を変更します。

1. パッケージをインストール

npm i @react-router/fs-routes

app/routes.tsにファイベースルーティングの設定を追加する。

app/routes.ts
import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from '@react-router/fs-routes';

const routes: RouteConfig = flatRoutes(); 

export default routes;

上記を設定すると、例えばapp/routes/_app._index/route.tsxがトップページとして表示される。

shadcn/ui導入

1. パッケージをインストール

npx shadcn@latest init
✔ Which style would you like to use? › Default
✔ Which color would you like to use as the base color? › Zinc
✔ Would you like to use CSS variables for theming? … yes

2. components.jsonを設定

components.json
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "default",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/app.css",
    "baseColor": "zinc",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "~/components/shadcn",
    "utils": "~/lib/utils",
    "ui": "~/components/shadcn/ui",
    "lib": "~/lib",
    "hooks": "~/hooks"
  },
  "iconLibrary": "lucide"
}

3. コンポーネントを追加

例えばボタンを追加したい場合、下記コマンドで追加できます。

npx shadcn@latest add button

Storybook導入

1. パッケージをインストール

npx storybook@latest init

2. Storybook用のvite-storybook.config.tsを作成

vite-storybook.config.tsを作成します。

vite-storybook.config.ts
import { defineConfig, loadEnv } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');
  process.env = { ...process.env, ...env };
  return {
    plugins: [tsconfigPaths()],
  };
});

3. main.tsを修正

  1. storiesの対象パスを変更(appフォルダ内を対象とする)
  2. builderオプションにvite-storybook.config.tsを設定
.storybook\main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  stories: ['../app/**/*.mdx', '../app/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: [
    '@storybook/addon-onboarding',
    '@storybook/addon-essentials',
    '@chromatic-com/storybook',
    '@storybook/addon-interactions',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {
      builder: {
        viteConfigPath: 'vite-storybook.config.ts',
      },
    },
  },
};
export default config;

4. StorybookにTailwind CSSを適応する

  1. preview.tsに、Tailwind CSSのimportを追加
.storybook\preview.ts
import type { Preview } from '@storybook/react';
+ import '../app/app.css';

const preview: Preview = {
// ...
  1. postcss.config.jsを追加
postcss.config.js
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

5. 利用方法

動作確認用に、shadcn/uiボタンのstorybookを追加します。

app/components/shadcn/ui/button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './button';

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  argTypes: {
    variant: {
      control: { type: 'select' },
      options: [
        'default',
        'destructive',
        'outline',
        'secondary',
        'ghost',
        'link',
      ],
    },
    size: {
      control: { type: 'select' },
      options: ['default', 'sm', 'lg', 'icon'],
    },
    asChild: { control: 'boolean' },
  },
};

export default meta;

type Story = StoryObj<typeof Button>;

export const Default: Story = {
  args: {
    variant: 'default',
    size: 'default',
    children: 'Button',
  },
};

export const Destructive: Story = {
  args: {
    variant: 'destructive',
    size: 'default',
    children: 'Button',
  },
};

export const Outline: Story = {
  args: {
    variant: 'outline',
    size: 'default',
    children: 'Button',
  },
};

export const Secondary: Story = {
  args: {
    variant: 'secondary',
    size: 'default',
    children: 'Button',
  },
};

export const Ghost: Story = {
  args: {
    variant: 'ghost',
    size: 'default',
    children: 'Button',
  },
};

export const Link: Story = {
  args: {
    variant: 'link',
    size: 'default',
    children: 'Button',
  },
};

Storybookを起動して、TailWindCSSが適用されていればOKです。

npm run storybook

image

セットアップは以上となります。

Discussion