ルートを定義する
ここでは、ファイルシステムベースのルーターの基本について学習します。ルートの定義方法、パラメータとページレイアウトの使用方法を学習します。
ルーターは、プロジェクトのルートにreact-server.config.mjsまたはreact-server.config.jsonファイルを作成することで設定できます。設定ファイルは設定オブジェクトをエクスポートし、ルートのrootパスを含める必要があります。静的ファイルのpublicパスを指定することもできます。ファイルシステムベースのルーターは、ルートディレクトリ内のファイルを自動的にスキャフォールドし、ルートディレクトリ内のファイルに基づいてルーティングを準備および構築します。
export default {
root: "src/pages",
public: "public",
};
ディレクトリとファイルは、アプリケーションが使用するルートを指定します。ルーターには、ディレクトリとファイルの名前をルートパスとルートパラメータに変換する規則が組み込まれています。新しいルートを作成するには、ルートディレクトリにファイルを作成する必要があります。
ルートファイルは、ルートをレンダリングするために使用されるReactコンポーネントをデフォルトでエクスポートする必要があります。デフォルトではページはサーバーコンポーネントです。ページの先頭に"use client"ディレクティブがある場合、クライアント専用ルートになります — ナビゲーションはサーバーラウンドトリップなしで完全にクライアント上で行われ、コンポーネントの状態はナビゲーション間で保持されます。
export default function Home() {
return <h1>Home</h1>;
}
ファイルにindexまたはpageという名前を付けると、インデックスルートとして扱われます。インデックスルートは、ディレクトリのデフォルトルートです。たとえば、ルートディレクトリにindex.jsxという名前のファイルを作成すると、アプリケーションのデフォルトルートとして扱われます。about という名前のディレクトリにindex.jsx という名前のファイルを作成すると、aboutディレクトリのデフォルトルートとして扱われます。
ファイルにindexまたはpage以外の名前を付けると、名前付きルートとして扱われます。たとえば、ルートディレクトリにabout.jsxという名前のファイルを作成すると、パス/aboutを持つ名前付きルートとして扱われます。usersという名前のディレクトリにabout.jsxという名前のファイルを作成すると、パス/users/aboutを持つ名前付きルートとして扱われます。
ルートディレクトリにさらにディレクトリを作成することで、ネストされたルートを作成できます。たとえば、ルートディレクトリにusersという名前のディレクトリを作成し、usersディレクトリにindex.jsxという名前のファイルを作成すると、パス/usersを持つネストされたルートとして扱われます。usersディレクトリにabout.jsxという名前のファイルを作成すると、パス/users/aboutを持つネストされたルートとして扱われます。
ファイル名に括弧で囲んだパラメータ名を追加することで、ルートパラメータを作成できます。たとえば、ルートディレクトリに[id].jsxという名前のファイルを作成すると、パス/[id]を持つ名前付きルートとして扱われます。usersというディレクトリに[id].jsxという名前のファイルを作成すると、パス/users/[id]を持つ名前付きルートとして扱われます。ネストされたディレクトリを作成することで、ネストされたルートパラメータを作成することもできます。たとえば、ルートディレクトリにusersというディレクトリを作成し、usersディレクトリに[id].jsxという名前のファイルを作成すると、パス/users/[id]を持つネストされたルートとして扱われます。コンポーネント内のルートパラメータにReact propsとしてアクセスできるようになります。
export default function User({ id }) {
return <h1>User #{id}</h1>;
}
単一のルートセグメントで複数のルートパラメータを使用することもできます。たとえば[id]-[name].jsxという名前のファイルを作成すると、/[id]-[name]というパスパターンを持つルートとして扱われ、コンポーネントでidとnameの両方がプロパティとして受け取られます。
export default function User({ id, name }) {
return <h1>User #{id} - {name}</h1>;
}
ファイル名に括弧で囲んだパラメータ名を追加することで、複数セグメントのルートパラメータを作成できます。たとえば、ルートディレクトリに[...id].jsxという名前のファイルを作成すると、パス/[...id]を持つ名前付きルートとして扱われます。users という名前のディレクトリに[...id].jsxという名前のファイルを作成すると、パス/users/[...id]を持つ名前付きルートとして扱われます。ネストされたディレクトリを作成することで、ネストされたルートパラメータを作成することもできます。たとえば、ルートディレクトリにusersという名前のディレクトリを作成し、usersディレクトリに[...id].jsxという名前のファイルを作成すると、パス/users/[...id]を持つネストされたルートとして扱われます。
実行時のパラメータは文字列の配列になります。コンポーネント内のルートパラメータにReactプロパティとしてアクセスできるようになります。
// /[...slug].page.jsx
export default function Page({ slug }) {
return <h1>/{slug.join("/")}</h1>;
}
ファイル名に括弧で囲んだパラメータ名を追加することで、オプションセグメントのルートパラメータを作成できます。たとえば、ルートディレクトリに[[...id]].jsxという名前のファイルを作成すると、パス/[[...id]]を持つ名前付きルートとして扱われます。usersという名前のディレクトリに[[...id]].jsxという名前のファイルを作成すると、パス/users/[[...id]]を持つ名前付きルートとして扱われます。ネストされたディレクトリを作成することで、ネストされたルートパラメータを作成することもできます。たとえば、ルートディレクトリにusersという名前のディレクトリを作成し、usersディレクトリに[[...id]].jsxという名前のファイルを作成すると、パス/users/[[...id]]を持つネストされたルートとして扱われます。
省略: ディレクトリ名またはファイル名の一部を括弧で囲むことで省略できます。たとえば、ルートディレクトリに
(404).[[...slug]].page.mdxという名前のファイルを作成すると、パス/[[...slug]]を持つルートとして扱われます。これを使用することで、ルートパスに影響を与えずディレクトリ名またはファイル名に情報を追加できます。
ファイル名にlayout.jsxを含むファイルを作成することで、レイアウトを作成できます。レイアウトファイルは、レイアウトファイルと同じディレクトリ内のすべてのルートをラップするために使用されます。サブディレクトリにlayout.jsxという名前のファイルを作成することで、ネストされたレイアウトを作成することもできます。レイアウトファイル名に省略された部分を使用することもできます。たとえば、ルートディレクトリに(root).layout.jsxという名前のファイルを作成すると、ルートディレクトリ内のすべてのルートのレイアウトとして使用されます。
レイアウトコンポーネントは、ルートコンポーネントをレンダリングするために使用する必要があるchildrenプロパティを受け取ります。
export default function Layout({ children }) {
return (
<>
<h1>Layout</h1>
{children}
</>
);
}
透過セグメントは、URLにレンダリングされないセグメントですが、ファイルを自分で識別するために使用されます。透過セグメントは(transparent).page.jsxという名前のファイルを作成することで使用できます。ここで、(transparent)は透過セグメントの名前で、任意の名前にすることができます。たとえば、ルートディレクトリに(main).page.jsxという名前のファイルを作成すると、パス/のルートとして扱われます。usersという名前のディレクトリに(main).page.jsxという名前のファイルを作成すると、パス/usersのルートとして扱われます。また、ディレクトリ構造で透過セグメントを使用して、ファイルをグループ化することもできます。たとえば、(dashboard)/usersという名前のディレクトリにpage.jsxという名前のファイルを作成すると、パス/usersのルートとして扱われます。
src - (root).layout.jsx - (root).page.jsx - (dashboard) - users - (users).page.jsx - [userId].page.jsx
ルートセグメントを中括弧で囲むことで、ルートセグメントをエスケープできます。たとえば、ルートディレクトリに{sitemap.xml}.server.mjsという名前のファイルを作成すると、パス/sitemap.xmlを持つ名前付きルートとして扱われます。
例: ファイルシステムベースのルーティングの基本的な例については、examples ディレクトリのPhotosの例を参照してください。
ファイルシステムベースのルーターでは、ページファイルからvalidateオブジェクトをエクスポートすることで、ルートパラメータと検索パラメータをランタイムでバリデーションできます。ランタイムがこのエクスポートを自動的に検出します — 追加の設定は不要です。
validateエクスポートは、オプションのparamsとsearchキーを持つオブジェクトで、各キーには任意の互換ライブラリ(Zod、ArkType、Valibot、またはValidateSchemaインターフェースを満たすもの)のスキーマを設定します。
pages/user/[id].page.tsximport { z } from "zod";
import { user } from "@lazarv/react-server/routes";
export const validate = {
params: z.object({
id: z.string().regex(/^\d+$/, "ID must be numeric"),
}),
};
export default user.createPage(({ id }) => {
// idはstring型で、/^\d+$/にマッチすることが保証される
return <h1>User {id}</h1>;
});
バリデーションが存在する場合、生成されたTypeScript型はスキーマからパラメータと検索型を推論します — そのためuseParams()、.Link、createPageはすべて生の文字列ではなくバリデーション済みの型を受け取ります。
ArkTypeの場合:
pages/user/[id].page.tsximport { type } from "arktype";
import { user } from "@lazarv/react-server/routes";
export const validate = {
params: type({ id: /^\d+$/ }),
};
export default user.createPage(({ id }) => {
return <h1>User {id}</h1>;
});
検索パラメータのバリデーション:
pages/products.page.tsximport { z } from "zod";
import { products } from "@lazarv/react-server/routes";
export const validate = {
search: z.object({
sort: z.enum(["name", "price", "rating"]).catch("name"),
page: z.coerce.number().int().positive().catch(1),
}),
};
export default products.createPage(() => {
const { sort, page } = products.useSearchParams();
return <div>Sorted by {sort}, page {page}</div>;
});
バリデーションが失敗した場合(例:上記の数値バリデーションで/user/abc)、useParams()はnullを返し、useSearchParams()は空のオブジェクト{}を返します。これにより、コンポーネントでエラーを処理したり、フォールバックルートに遷移させたりできます。
ファイルシステムベースのルーターを使用すると、ランタイムは仮想的な@lazarv/react-server/routesモジュールを自動的に生成します。このモジュールはアプリケーション内のすべてのルートに対して名前付きルートディスクリプタをエクスポートします — ファイル構造から導出され、開発中はリアルタイムで更新され、各ビルドで再生成されます。
import { index, about, user, dashboard } from "@lazarv/react-server/routes";
各エクスポートされたディスクリプタは、createRouteで作成されたものと同じ機能を持つ完全なRouteDescriptorです:
.Link— コンパイル時のパラメータチェック付き型付きLinkコンポーネント.href(params?)— 型付きパラメータからURLパス名を構築.useParams()— 型付きでバリデーション済みのパラメータを読み取る(サーバーとクライアントコンポーネントで動作).useSearchParams()— 型付きでバリデーション済みの検索パラメータを読み取る.SearchParams— ルートスコープの検索パラメータ変換境界
さらに、ページコンポーネントの型付けのためのコンテキスト対応ヘルパー関数:
.createPage(component)— バリデーション済みパラメータをプロップとする型付きページコンポーネント.createLayout(component)—childrenとブランド付きアウトレットプロップを持つ型付きレイアウト.createLoading(component)— 型付きローディングフォールバックコンポーネント.createError(component)— 型付きエラーバウンダリコンポーネント.createFallback(component)— 型付きフォールバックコンポーネント.createMiddleware(handler)— リクエストパラメータ付きの型付きミドルウェア
これらのヘルパーはランタイムではアイデンティティ関数です — コンポーネントをそのまま返します。ルートパス、validateエクスポート、アウトレット構造から正しいプロップ型を推論するためにTypeScript専用で存在します。
ルート名はファイルパスからキャメルケースで自動的に導出されます:
| ファイルパス | エクスポート名 |
|---|---|
pages/page.tsx or pages/index.tsx | index |
pages/about.tsx | about |
pages/user/[id].page.tsx | user |
pages/user/posts.page.tsx | userPosts |
pages/dashboard/settings.page.tsx | dashboardSettings |
[id]のような動的セグメントは名前から除去されます。pages/[id].page.tsxのような純粋に動的なパスでは、パラメータ名(id)がルート名として使用されます。
ページファイルからroute定数をエクスポートすることで、自動導出された名前をオーバーライドできます:
export const route = "userProfile";
エクスポートされる名前は自動導出された名前の代わりにuserProfileになります:
import { userProfile } from "@lazarv/react-server/routes";
createPageを使用して、ルートのパスパラメータとvalidateエクスポートから推論された型付きプロップを取得します:
pages/user/[id].page.tsximport { z } from "zod";
import { user } from "@lazarv/react-server/routes";
export const route = "user";
export const validate = {
params: z.object({ id: z.string().regex(/^\d+$/, "ID must be numeric") }),
};
// TypeScriptが推論: ({ id: string }) => ReactNode
export default user.createPage(({ id }) => {
return <h1>User {id}</h1>;
});
validateなしの場合、パラメータ型はパスパターンから来ます — [id]は{ id: string }、[...slug]は{ slug: string[] }を返します。validateありの場合、型はスキーマの出力型から推論され、より狭い型になり得ます(例:正規表現制約付きの文字列、変換後の数値)。
動的パラメータのないページの場合、createPageは空のプロップオブジェクトを受け取ります:
pages/about.tsximport { about } from "@lazarv/react-server/routes";
export default about.createPage(() => {
return <h1>About</h1>;
});
createLayoutを使用して、型付きchildrenとアウトレットプロップを取得します。アウトレットはレイアウトのディレクトリ配下の@outletNameディレクトリにファイルを配置して作成します:
pages/
├── dashboard/
│ ├── (dashboard).layout.tsx
│ ├── index.page.tsx
│ ├── @sidebar/
│ │ └── nav.page.tsx
│ └── @content/
│ ├── @content.default.tsx # ルートがマッチしない時のデフォルトコンテンツ
│ └── feed.page.tsx
レイアウトは型付きアウトレットプロップを受け取ります — 各アウトレットはTypeScriptが個別に追跡するブランド付きReact.ReactElementで、混在を防ぎます:
pages/dashboard/(dashboard).layout.tsximport { dashboard } from "@lazarv/react-server/routes";
export default dashboard.createLayout(({ children, sidebar, content }) => {
return (
<div>
<h1>Dashboard</h1>
<div style={{ display: "grid", gridTemplateColumns: "200px 1fr" }}>
<aside>{sidebar}</aside>
<main>
<div>{content}</div>
<div>{children}</div>
</main>
</div>
</div>
);
});
Nullableアウトレット: アウトレットディレクトリに@outletName.default.tsxファイルがある場合、アウトレットは常にレンダリングされます(non-nullable)。デフォルトがない場合、アウトレットはReactElement | nullになります — TypeScriptがnullチェックを強制します。
上の例では、contentにはデフォルト(@content.default.tsx)があるため常にReactElementです。sidebarにデフォルトがない場合、ReactElement | nullとして型付けされます。
ローディングとエラーコンポーネントも同じパターンに従います:
pages/user/[id].loading.tsximport { user } from "@lazarv/react-server/routes";
export default user.createLoading(() => {
return <div>Loading user profile...</div>;
});
pages/user/[id].error.tsximport { user } from "@lazarv/react-server/routes";
export default user.createError(({ error }) => {
return <div>Error loading user: {error.message}</div>;
});
ルートモジュールからのルートディスクリプタはcreateRouteディスクリプタと同様に動作します — ナビゲーションには.Linkを、URL構築には.href()を使用します:
pages/(root).layout.tsximport { index, about, user, dashboard } from "@lazarv/react-server/routes";
export default index.createLayout(({ children }) => {
return (
<html lang="en">
<body>
<nav>
<index.Link>Home</index.Link>
<about.Link>About</about.Link>
<user.Link params={{ id: "42" }}>User 42</user.Link>
<dashboard.Link>Dashboard</dashboard.Link>
</nav>
{children}
</body>
</html>
);
});
動的セグメントのないルート(index、aboutなど)はparamsを受け付けません。動的セグメントを持つルート(userなど)は必須です — TypeScriptがコンパイル時にこれを強制します。
ランタイムは.react-serverディレクトリにreact-server-routes.d.tsファイルを書き込みます。このファイルはルートごとのインターフェースで@lazarv/react-server/routesモジュールを宣言します:
// 自動生成 — 手動で編集しないでください
declare module "@lazarv/react-server/routes" {
import type { RouteDescriptor, ValidateSchema } from "@lazarv/react-server/router";
// ── /user/[id] ──
interface UserRoute extends RouteDescriptor<"/user/[id]",
typeof import("../pages/user/[id].page").validate extends
{ params: ValidateSchema<infer T> } ? T : { id: string },
Record<string, string>
> {
createPage(
component: (props: /* 推論されたパラメータ型 */) => React.ReactNode
): typeof component;
createLoading(
component: () => React.ReactNode
): typeof component;
}
export const user: UserRoute;
}
生成される型には、各ルートに存在するヘルパーのみが含まれます — ルートにページファイルがあればcreatePageが、レイアウトがあれば正しいアウトレットプロップ付きのcreateLayoutが得られます。このファイルは開発中とビルド時に自動的に再生成されるため、常にファイル構造と一致します。
注意: ルートモジュールをバリデーション、レイアウト、アウトレット、ローディング状態、クライアント専用ルートと組み合わせた完全な動作アプリケーションについては、typed-file-routerのサンプルをチェックしてください。
プログラム的な型付きルーティングAPI(
createRoute、createRouter、スキーマバリデーション戦略、パース関数など)については、型付きルーターページを参照してください。
TypeScriptでファイルシステムベースのルーターを使用すると、ランタイムはファイル構造から型定義を自動的に生成します。これらの型は以下をカバーします:
Link、navigate、replace、prefetchのすべての静的および動的ルートパス[id]や[...slug]などの動的セグメントから抽出されたルートパラメータ型ReactServerComponentで利用可能なアウトレット名
型チェックを有効にするには、生成された型をTypeScript設定に追加します:
tsconfig.json{
"compilerOptions": {
// ...
},
"include": [/* ... */, ".react-server/**/*.ts"]
}
それだけです。ランタイムはファイル構造が変更されるたびに開発中に自動的に型を再生成し、ビルド時にも再生成します。IDEはすべてのルートパスのオートコンプリートを即座に表示し、無効なルートをコンパイル時にフラグします。
例えば、次のファイル構造がある場合:
app/
├── page.tsx # /
├── about.tsx # /about
├── user/
│ ├── [id].tsx # /user/:id
│ └── page.tsx # /user
└── posts/
└── [...slug].tsx # /posts/*
Linkコンポーネントとnavigate関数は、/、/about、/user、/postsを静的ルートとしてオートコンプリートし、/user/[id]と/posts/[...slug]の型付きパラメータを提供します。
プログラム的な型付きルーティングAPI(createRoute、createRouter、型付きフック、スキーマバリデーションなど)については、型付きルーターを参照してください。