React Router v7 のルーティング設計:入門から実践まで

React Router v7 は、これまで Remix が担っていたフルスタック機能を取り込み、「ライブラリとしても」「フレームワークとしても」使えるルーターへと進化しました。本記事では React Router をはじめて触る方に向けて、v7 のルーティング設計を基礎から整理します。

この記事で扱う範囲

  • v7 の 2 つの使い方(library モード / framework モード)
  • ルート定義の書き方
  • ネストルートとレイアウト
  • データローダー(loader)とアクション(action)
  • 型安全なルーティング
  • v6 からの移行時に押さえるポイント

動作環境

  • react / react-dom 18 以降
  • react-router / react-router-dom v7
  • TypeScript 5 系
  • Node.js 20 以降

※ バージョンは記事執筆時点の一般的な構成です。最新の対応バージョンは公式ドキュメントをご確認ください。

React Router v7 の全体像

v7 には大きく 2 つの使い方があります。

モード 特徴 向いているケース
Library モード v6 までと同じく、既存の SPA にルーターとして組み込む CSR 中心の SPA、既存アプリへの追加
Framework モード ファイルベースルーティング・SSR・型生成を含むフルスタック構成 新規で SSR/SSG も視野に入れる場合

重要なのは、どちらも同じルーティングの概念(ネストルート・loader・action)を共有している点です。まず library モードで概念を押さえ、必要になったら framework モードへ、という順序で学ぶのがおすすめです。

Library モード:ルート定義の基本

最もシンプルなのは createBrowserRouter にルート定義の配列を渡す方法です。

// src/router.tsx
import { createBrowserRouter } from "react-router-dom";
import Root from "./routes/Root";
import Home from "./routes/Home";
import About from "./routes/About";
import NotFound from "./routes/NotFound";

export const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    errorElement: <NotFound />,
    children: [
      { index: true, element: <Home /> },
      { path: "about", element: <About /> },
    ],
  },
]);
// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { router } from "./router";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <RouterProvider router={router} />
  </StrictMode>
);

ポイントは children によるネストです。/about は Root の内側に描画されます。

ネストルートとレイアウト

ネストルートは「URL の階層」と「UI の階層」を一致させる仕組みです。親ルートは共通レイアウトを担い、子ルートの描画位置を <Outlet /> で指定します。

// src/routes/Root.tsx
import { Link, Outlet } from "react-router-dom";

export default function Root() {
  return (
    <div>
      <header>
        <nav>
          <Link to="/">Home</Link>
          <Link to="/about">About</Link>
        </nav>
      </header>
      <main>
        {/* 子ルートがここに描画される */}
        <Outlet />
      </main>
    </div>
  );
}

この設計により、ヘッダーやサイドバーを重複して書かずに済みます。「画面の共通部分 = 親ルート」「切り替わる部分 = 子ルート」と考えると整理しやすくなります。

レイアウト専用ルート(path なしルート)

URL を持たず、レイアウトだけをグループ化したい場合は path を省略します。

{
  element: <DashboardLayout />,
  children: [
    { path: "dashboard", element: <Dashboard /> },
    { path: "settings", element: <Settings /> },
  ],
}

/dashboard と /settings は URL 上は階層を持ちませんが、UI としては同じレイアウトを共有します。

データローダー(loader)

v7 の大きな特徴が、描画前にデータを取得する loader です。コンポーネント内の useEffect でフェッチする従来パターンと違い、遷移が確定した時点でデータ取得が走るため、ちらつきや取得漏れを抑えられます。

// src/routes/User.tsx
import { useLoaderData, type LoaderFunctionArgs } from "react-router-dom";

export async function loader({ params }: LoaderFunctionArgs) {
  const res = await fetch(`/api/users/${params.userId}`);
  if (!res.ok) {
    throw new Response("Not Found", { status: 404 });
  }
  return { user: await res.json() };
}

export default function User() {
  const { user } = useLoaderData() as { user: { id: string; name: string } };
  return <h1>{user.name}</h1>;
}

loader の中で throw new Response(..., { status: 404 }) のように投げると、最も近い errorElement が描画されます。エラーハンドリングをルート単位で宣言的に書けるのが利点です。

アクション(action)とフォーム送信

データ更新は action で扱います。<Form> コンポーネントと組み合わせると、ページ全体をリロードせずに POST 相当の処理を実行できます。

// src/routes/NewPost.tsx
import { Form, redirect, type ActionFunctionArgs } from "react-router-dom";

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const title = formData.get("title");

  await fetch("/api/posts", {
    method: "POST",
    body: JSON.stringify({ title }),
    headers: { "Content-Type": "application/json" },
  });

  return redirect("/posts");
}

export default function NewPost() {
  return (
    <Form method="post">
      <input name="title" />
      <button type="submit">作成</button>
    </Form>
  );
}

loader が読み取り、action が書き込み、という役割分担で考えると設計が明快になります。送信後に関連する loader が自動で再実行されるため、更新後のデータ再取得を自分で書く必要がありません。

Framework モード:ファイルベースルーティング

framework モードでは app/routes.ts でルートを宣言します。ファイル名規約に頼らず、明示的に書けるのが v7 の特徴です。

// app/routes.ts
import { type RouteConfig, index, route, layout } from "@react-router/dev/routes";

export default [
  index("routes/home.tsx"),
  route("about", "routes/about.tsx"),
  layout("routes/dashboard/layout.tsx", [
    route("dashboard", "routes/dashboard/index.tsx"),
    route("dashboard/settings", "routes/dashboard/settings.tsx"),
  ]),
] satisfies RouteConfig;

index / route / layout というヘルパで、library モードの配列と同じ構造を型安全に表現できます。

型安全なルート(型生成)

framework モードでは、各ルートの loader 戻り値や params の型が自動生成され、Route.ComponentProps 経由で受け取れます。

// app/routes/user.tsx
import type { Route } from "./+types/user";

export async function loader({ params }: Route.LoaderArgs) {
  const res = await fetch(`/api/users/${params.userId}`);
  return { user: await res.json() };
}

export default function User({ loaderData }: Route.ComponentProps) {
  return <h1>{loaderData.user.name}</h1>;
}

params.userId が string として、loaderData が loader の戻り値として型付けされます。URL パラメータの typo やデータ型のずれをコンパイル時に検出できるのは大きな利点です。

v6 からの移行で押さえるポイント

  • import 元の統一:多くの API が react-router に集約されつつあります。react-router-dom からの import が非推奨になるものもあるため、公式の移行ガイドを確認してください。
  • loader / action への寄せ替え:useEffect フェッチを少しずつ loader へ移すと、v7 の恩恵(事前取得・自動再取得)を受けやすくなります。
  • library → framework の移行は段階的に可能:いきなり全面移行する必要はありません。

動作確認のポイント

  • / にアクセスして Root のレイアウト内に Home が描画されるか
  • /about でナビゲーションが崩れず子だけ切り替わるか
  • 存在しない userId で 404(errorElement)が出るか
  • フォーム送信後に一覧が自動で最新化されるか(action → loader 再実行)
  • framework モードで params / loaderData に型が付いているか

注意点

  • loader は遷移のたびに走ります。重い処理はキャッシュ戦略(shouldRevalidate など)を検討してください。
  • loader 内の fetch 失敗は例外として扱い、errorElement に寄せると UI が安定します。
  • library と framework はモードとして別物に見えますが、ルーティングの概念は共通です。まずネストルートと loader/action を理解すれば、どちらでも応用できます。

まとめ

React Router v7 のルーティング設計は、「ネストルートで UI 階層を表現し、loader/action でデータの読み書きを宣言的に扱う」という一貫した考え方に支えられています。library モードで基礎を押さえ、SSR や型安全が必要になったら framework モードへ段階的に進む——この順序が、v7 を無理なく習得する近道です。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。