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 を無理なく習得する近道です。
