はじめに#
React Routerは、Reactアプリケーションにおけるルーティングのデファクトスタンダードです。従来は設定ファイルやJSXでルートを定義する方法が主流でしたが、React Router v7からは@react-router/fs-routesパッケージを利用することで、Next.jsやRemixのようなファイルベースルーティングを実現できます。
React Routerのファイルベースルーティングでは、app/routesディレクトリ内のファイル名がそのままURLパスに対応します。これにより、ルート設定ファイルを個別に記述する手間が省け、プロジェクト構造とURL構造を直感的に一致させることができます。
本記事では、React Routerでファイルベースルーティングを導入する方法から、動的セグメント・ネストルート・レイアウトなどの実践的なパターンまで、サンプルコードとともに詳しく解説します。
実行環境と前提条件#
必要な環境#
| 項目 |
バージョン・要件 |
| Node.js |
20.x 以上 |
| npm |
10.x 以上 |
| React Router |
7.x |
| TypeScript |
5.x(推奨) |
前提知識#
- Reactコンポーネントの基本的な作成方法
- ES6+のJavaScript/TypeScript構文
- CLIでのnpmコマンド操作
プロジェクトの作成#
React Routerプロジェクトを新規作成するには、以下のコマンドを実行します。
1
2
3
4
|
npx create-react-router@latest my-app
cd my-app
npm install
npm run dev
|
http://localhost:5173でアプリケーションが起動します。
React Routerファイルベースルーティングの概要#
アーキテクチャ#
React Routerのファイルベースルーティングは、以下の構成要素で成り立っています。
graph TD
A[app/routes.ts] -->|設定エントリ| B[flatRoutes関数]
B -->|スキャン| C[app/routes/]
C --> D[_index.tsx]
C --> E[about.tsx]
C --> F[blog.$slug.tsx]
D -->|生成| G["/ ルート"]
E -->|生成| H["/about ルート"]
F -->|生成| I["/blog/:slug ルート"]@react-router/fs-routesパッケージのflatRoutes関数がapp/routesディレクトリをスキャンし、ファイル名の規約に基づいてルート設定を自動生成します。
設定ベースとの違い#
従来の設定ベースルーティングでは、以下のように明示的にルートを定義します。
1
2
3
4
5
6
7
8
|
// 設定ベースの例(app/routes.ts)
import { type RouteConfig, route, index } from "@react-router/dev/routes";
export default [
index("./home.tsx"),
route("about", "./about.tsx"),
route("blog/:slug", "./blog-post.tsx"),
] satisfies RouteConfig;
|
ファイルベースルーティングでは、ディレクトリ構造がそのままルート定義になります。
1
2
3
4
|
app/routes/
├── _index.tsx → /
├── about.tsx → /about
└── blog.$slug.tsx → /blog/:slug
|
@react-router/fs-routesのセットアップ#
パッケージのインストール#
ファイルベースルーティングを有効にするには、@react-router/fs-routesパッケージをインストールします。
1
|
npm install @react-router/fs-routes
|
routes.tsの設定#
app/routes.tsファイルでflatRoutes関数を使用します。
1
2
3
4
5
|
// app/routes.ts
import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default flatRoutes() satisfies RouteConfig;
|
この設定により、app/routesディレクトリ内のファイルが自動的にルートとして認識されます。
オプション設定#
flatRoutes関数は、以下のオプションを受け取ります。
1
2
3
4
5
6
7
8
9
10
|
// app/routes.ts
import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default flatRoutes({
// ルートファイルを探索するディレクトリ(デフォルト: "routes")
rootDirectory: "routes",
// ルートとして認識しないファイルパターン
ignoredRouteFiles: ["**/components/**", "**/*.test.tsx"],
}) satisfies RouteConfig;
|
ハイブリッド構成#
設定ベースとファイルベースを組み合わせることも可能です。
1
2
3
4
5
6
7
8
9
10
11
|
// app/routes.ts
import { type RouteConfig, route } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default [
// 設定ベースのルート
route("/", "./home.tsx"),
// ファイルベースのルート(app/routesディレクトリ)
...(await flatRoutes()),
] satisfies RouteConfig;
|
ファイル命名規約#
基本ルート#
ファイル名がそのままURLパスに対応します。
| ファイル名 |
URLパス |
_index.tsx |
/ |
about.tsx |
/about |
contact.tsx |
/contact |
_index.tsxは特別なファイルで、親ルートのインデックスページとして機能します。
1
2
3
4
5
6
7
8
9
|
// app/routes/_index.tsx
export default function HomePage() {
return (
<div>
<h1>ホームページへようこそ</h1>
<p>React Routerのファイルベースルーティングを体験しましょう。</p>
</div>
);
}
|
ドット区切りによるパス階層#
ファイル名に.(ドット)を使用すると、URLの/に変換されます。
| ファイル名 |
URLパス |
concerts.trending.tsx |
/concerts/trending |
concerts.salt-lake-city.tsx |
/concerts/salt-lake-city |
api.users.list.tsx |
/api/users/list |
1
2
3
4
5
6
7
8
9
|
// app/routes/concerts.trending.tsx
export default function TrendingConcerts() {
return (
<div>
<h1>トレンドのコンサート</h1>
<p>今話題のコンサート情報を表示します。</p>
</div>
);
}
|
動的セグメント($プレフィックス)#
$で始まるセグメントは動的パラメータになります。
| ファイル名 |
URLパス |
パラメータ |
concerts.$city.tsx |
/concerts/:city |
params.city |
users.$userId.tsx |
/users/:userId |
params.userId |
blog.$year.$month.$slug.tsx |
/blog/:year/:month/:slug |
params.year, params.month, params.slug |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// app/routes/concerts.$city.tsx
import type { Route } from "./+types/concerts.$city";
export async function loader({ params }: Route.LoaderArgs) {
const concerts = await fetchConcertsByCity(params.city);
return { concerts, city: params.city };
}
export default function CityPage({ loaderData }: Route.ComponentProps) {
const { concerts, city } = loaderData;
return (
<div>
<h1>{city}のコンサート一覧</h1>
<ul>
{concerts.map((concert) => (
<li key={concert.id}>{concert.name}</li>
))}
</ul>
</div>
);
}
|
オプショナルセグメント#
()で囲むと、そのセグメントはオプショナル(任意)になります。
| ファイル名 |
マッチするURL |
($lang)._index.tsx |
/, /en, /ja |
($lang).about.tsx |
/about, /en/about, /ja/about |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// app/routes/($lang)._index.tsx
import type { Route } from "./+types/($lang)._index";
export async function loader({ params }: Route.LoaderArgs) {
const lang = params.lang || "ja";
return { lang };
}
export default function HomePage({ loaderData }: Route.ComponentProps) {
return (
<div>
<h1>言語: {loaderData.lang}</h1>
</div>
);
}
|
スプラットルート(キャッチオール)#
$.tsxまたはfiles.$.tsxのように$を使用すると、残りのパスすべてをキャッチします。
| ファイル名 |
マッチするURL |
$.tsx |
すべての未定義ルート(404ページ用) |
files.$.tsx |
/files/*(例: /files/docs/readme.md) |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// app/routes/files.$.tsx
import type { Route } from "./+types/files.$";
export async function loader({ params }: Route.LoaderArgs) {
const filePath = params["*"];
const fileInfo = await getFileInfo(filePath);
return { filePath, fileInfo };
}
export default function FilePage({ loaderData }: Route.ComponentProps) {
const { filePath, fileInfo } = loaderData;
return (
<div>
<h1>ファイル: {filePath}</h1>
<pre>{JSON.stringify(fileInfo, null, 2)}</pre>
</div>
);
}
|
ネストルートの実装#
自動ネスト#
ドット区切りのファイル名で、親ルートと同じプレフィックスを持つファイルは自動的にネストされます。
1
2
3
4
5
|
app/routes/
├── concerts.tsx ← 親ルート
├── concerts._index.tsx ← /concerts(インデックス)
├── concerts.$city.tsx ← /concerts/:city
└── concerts.trending.tsx ← /concerts/trending
|
親ルートには<Outlet />を配置して、子ルートをレンダリングします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// app/routes/concerts.tsx
import { Outlet, Link } from "react-router";
export default function ConcertsLayout() {
return (
<div>
<header>
<h1>コンサート情報</h1>
<nav>
<Link to="/concerts">ホーム</Link>
<Link to="/concerts/trending">トレンド</Link>
</nav>
</header>
<main>
{/* 子ルートがここにレンダリングされる */}
<Outlet />
</main>
</div>
);
}
|
1
2
3
4
5
6
7
8
9
|
// app/routes/concerts._index.tsx
export default function ConcertsIndex() {
return (
<div>
<h2>コンサートトップページ</h2>
<p>お気に入りのコンサートを探しましょう。</p>
</div>
);
}
|
期待される動作として、/concertsにアクセスすると、concerts.tsxのレイアウト内にconcerts._index.tsxがレンダリングされます。
1
2
3
|
<ConcertsLayout>
<ConcertsIndex />
</ConcertsLayout>
|
レイアウトなしのネストURL#
_サフィックスを使用すると、URLはネストされますが、レイアウトはネストされません。
1
2
3
4
|
app/routes/
├── concerts.tsx
├── concerts.$city.tsx
└── concerts_.mine.tsx ← レイアウトネストなし
|
concerts_.mine.tsxは/concerts/mineというURLになりますが、concerts.tsxのレイアウトは使用されず、直接root.tsxにレンダリングされます。
1
2
3
4
5
6
7
8
9
|
// app/routes/concerts_.mine.tsx
export default function MyConcerts() {
return (
<div>
<h1>マイコンサート</h1>
<p>独自レイアウトでレンダリングされます。</p>
</div>
);
}
|
URLなしのレイアウトネスト(パスレスルート)#
_プレフィックスを使用すると、URLパスを追加せずにレイアウトを共有できます。
1
2
3
4
|
app/routes/
├── _auth.tsx ← URLに影響しないレイアウト
├── _auth.login.tsx ← /login
└── _auth.register.tsx ← /register
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// app/routes/_auth.tsx
import { Outlet } from "react-router";
export default function AuthLayout() {
return (
<div className="auth-container">
<div className="auth-logo">
<img src="/logo.png" alt="ロゴ" />
</div>
<div className="auth-form">
<Outlet />
</div>
</div>
);
}
|
1
2
3
4
5
6
7
8
9
10
11
|
// app/routes/_auth.login.tsx
export default function LoginPage() {
return (
<form>
<h1>ログイン</h1>
<input type="email" placeholder="メールアドレス" />
<input type="password" placeholder="パスワード" />
<button type="submit">ログイン</button>
</form>
);
}
|
フォルダによる整理#
route.tsxパターン#
ルートをフォルダで整理する場合、route.tsxファイルをメインモジュールとして使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
app/routes/
├── _index/
│ ├── route.tsx ← メインルートモジュール
│ ├── hero-section.tsx ← ローカルコンポーネント
│ └── feature-list.tsx ← ローカルコンポーネント
├── about/
│ ├── route.tsx
│ ├── team-member.tsx
│ └── styles.css
└── blog.$slug/
├── route.tsx
├── comments.tsx
└── share-buttons.tsx
|
1
2
3
4
5
6
7
8
9
10
11
12
|
// app/routes/_index/route.tsx
import { HeroSection } from "./hero-section";
import { FeatureList } from "./feature-list";
export default function HomePage() {
return (
<div>
<HeroSection />
<FeatureList />
</div>
);
}
|
1
2
3
4
5
6
7
8
9
|
// app/routes/_index/hero-section.tsx
export function HeroSection() {
return (
<section className="hero">
<h1>React Routerで作るモダンなSPA</h1>
<p>ファイルベースルーティングで効率的な開発を実現</p>
</section>
);
}
|
フォルダ内のroute.tsx以外のファイルはルートとして認識されないため、コンポーネントやスタイルを同じフォルダに配置できます。
特殊文字のエスケープ#
URLに特殊文字を含めたい場合は、[]でエスケープします。
| ファイル名 |
URLパス |
sitemap[.]xml.tsx |
/sitemap.xml |
reports.$id[.pdf].tsx |
/reports/:id.pdf |
dolla-bills-[$].tsx |
/dolla-bills-$ |
1
2
3
4
5
6
7
8
9
|
// app/routes/sitemap[.]xml.tsx
export async function loader() {
const sitemap = generateSitemap();
return new Response(sitemap, {
headers: {
"Content-Type": "application/xml",
},
});
}
|
ルートモジュールのエクスポート#
React Routerのルートモジュールでは、以下のエクスポートが利用できます。
loaderとaction#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// app/routes/posts.$id.tsx
import type { Route } from "./+types/posts.$id";
// データ取得(GETリクエスト)
export async function loader({ params }: Route.LoaderArgs) {
const post = await fetchPost(params.id);
if (!post) {
throw new Response("Not Found", { status: 404 });
}
return { post };
}
// データ更新(POST/PUT/DELETE)
export async function action({ request, params }: Route.ActionArgs) {
const formData = await request.formData();
await updatePost(params.id, formData);
return { success: true };
}
export default function PostPage({ loaderData }: Route.ComponentProps) {
const { post } = loaderData;
return <article>{post.content}</article>;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// app/routes/about.tsx
import type { Route } from "./+types/about";
export function meta({}: Route.MetaArgs) {
return [
{ title: "会社概要 | サイト名" },
{ name: "description", content: "私たちについて詳しくご紹介します。" },
];
}
export default function AboutPage() {
return <div>会社概要ページ</div>;
}
|
ErrorBoundary#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// app/routes/posts.$id.tsx
import { isRouteErrorResponse, useRouteError } from "react-router";
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
return (
<div>
<h1>エラーが発生しました</h1>
<p>予期しないエラーが発生しました。</p>
</div>
);
}
|
実践的なディレクトリ構成例#
以下は、ECサイトを想定したディレクトリ構成例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
app/
├── routes.ts
├── root.tsx
└── routes/
├── _index.tsx # /
├── _auth.tsx # 認証レイアウト
├── _auth.login.tsx # /login
├── _auth.register.tsx # /register
├── products._index.tsx # /products
├── products.$id.tsx # /products/:id
├── products.$id_.reviews.tsx # /products/:id/reviews(独自レイアウト)
├── cart.tsx # /cart
├── checkout/
│ ├── route.tsx # /checkout
│ ├── shipping-form.tsx
│ └── payment-form.tsx
├── ($lang).about.tsx # /about, /en/about, /ja/about
├── api.products.tsx # /api/products
└── $.tsx # 404 キャッチオール
|
この構成では、以下のルーティングが自動生成されます。
| URL |
ファイル |
説明 |
/ |
_index.tsx |
トップページ |
/login |
_auth.login.tsx |
ログイン(認証レイアウト内) |
/register |
_auth.register.tsx |
登録(認証レイアウト内) |
/products |
products._index.tsx |
商品一覧 |
/products/123 |
products.$id.tsx |
商品詳細 |
/products/123/reviews |
products.$id_.reviews.tsx |
レビュー(独自レイアウト) |
/cart |
cart.tsx |
カート |
/checkout |
checkout/route.tsx |
チェックアウト |
/about, /en/about |
($lang).about.tsx |
多言語対応の概要ページ |
/api/products |
api.products.tsx |
API エンドポイント |
| その他すべて |
$.tsx |
404ページ |
まとめ#
React Routerのファイルベースルーティングは、@react-router/fs-routesパッケージを使用することで簡単に導入できます。ファイル名の規約を覚えれば、ルート設定ファイルを手動で管理する必要がなくなり、プロジェクトの保守性が向上します。
本記事で解説した主要なポイントは以下のとおりです。
flatRoutes()関数でapp/routesディレクトリを自動スキャン
- ドット区切り(
.)でURLパスの階層を表現
$プレフィックスで動的セグメントを定義
_プレフィックス/サフィックスでレイアウトネストを制御
- フォルダ +
route.tsxパターンでコードを整理
ファイルベースルーティングを活用することで、直感的で保守しやすいReactアプリケーションを構築できます。
参考リンク#