ルーターこのページを編集.md

ルートを定義する

ここでは、ファイルシステムベースのルーターの基本について学習します。ルートの定義方法、パラメータとページレイアウトの使用方法を学習します。

ルーターは、プロジェクトのルートに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]というパスパターンを持つルートとして扱われ、コンポーネントでidnameの両方がプロパティとして受け取られます。

export default function User({ id, name }) { return <h1>User #{id} - {name}</h1>; }

マッチャーエイリアス: パラメータブラケットには=aliasサフィックスを付けることができます。たとえば[id=numeric].page.jsx/[id=numeric]。エイリアスはexport const matchersで指定した述語にセグメントを結び付け、ルートがマッチする前にセグメントの形状でゲートをかけることができます。後述のルートマッチャーを参照してください。プレーンな[id]はフィルタリングなしでパラメータを抽出します。

ファイル名に括弧で囲んだパラメータ名を追加することで、複数セグメントのルートパラメータを作成できます。たとえば、ルートディレクトリに[...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エクスポートは、オプションのparamssearchキーを持つオブジェクトで、各キーには任意の互換ライブラリ(Zod、ArkType、Valibot、またはValidateSchemaインターフェースを満たすもの)のスキーマを設定します。

pages/user/[id].page.tsx
import { 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().LinkcreatePageはすべて生の文字列ではなくバリデーション済みの型を受け取ります。

ArkTypeの場合:

pages/user/[id].page.tsx
import { 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.tsx
import { 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()は空のオブジェクト{}を返します。これにより、コンポーネントでエラーを処理したり、フォールバックルートに遷移させたりできます。

マッチャーは、URL がそもそもルートにマッチするかどうかをゲートする述語です。ルーティング時にページコードが読み込まれる前に実行され、同じパラメータ空間を形状によって分割する兄弟ルートを定義できます。

マッチャー構文を使用するには、ファイル名のパラメータブラケット内に=aliasサフィックスを追加します。エイリアスはexport const matchersで使用するキーになります。

ブラケット形式マッチャーが受け取る値
[name=alias][id=numeric].page.tsxstring
[[name=alias]][[tab=known]].page.tsxstring
[...name=alias][...slug=nested].page.tsxstring[]
[[...name=alias]][[...path=valid]].page.tsxstring[]

ページ(またはミドルウェアや API ルート)からmatchersオブジェクトをエクスポートします。キーはパスで使用しているエイリアスと一致させます。各値は URL を受け入れる場合はtrue、拒否する場合はfalseを返す述語です。拒否された URL は兄弟ルートにフォールスルーします — それ自体では 404 にはなりません。

pages/product/[sku=uppercase].page.tsx
import { productSkuUppercase } from "@lazarv/react-server/routes"; export const matchers = productSkuUppercase.createMatchers({ uppercase: (value) => /^[A-Z0-9-]+$/.test(value), }); export default productSkuUppercase.createPage(({ sku }) => { return <h1>Product {sku}</h1>; });

その隣に兄弟の[sku].page.tsxを配置すると、拒否された値を受け止めます。

pages/product/[sku].page.tsx
import { product } from "@lazarv/react-server/routes"; export default product.createPage(({ sku }) => { return <h1>Fallback: {sku}</h1>; });

生成されるルート名(productSkuUppercaseproduct)は、ルート命名で説明されている導出ルールから直接得られます。マッチャーでゲートされたセグメントはパラメータ名とエイリアスの両方を寄与させ、ベアなダイナミックセグメントは取り除かれます。明示的なrouteオーバーライドは不要です。

これで/product/ABC-123はマッチャーでゲートされたページをレンダリングし、/product/abc-123は兄弟にフォールスルーします。ランタイムはマッチャーでゲートされたルートを最初に試します — 同じパス形状でより具体的なルートは、常により具体的でない兄弟よりも上位にランクされます。

キャッチオールマッチャーは配列を受け取ります。 エイリアスサフィックスは[...slug][[...slug]]でも同様に動作しますが、述語はセグメントの配列全体を受け取り、文字列は受け取りません。

pages/docs/[...slug=nested].page.tsx
import { docsSlugNested } from "@lazarv/react-server/routes"; export const matchers = docsSlugNested.createMatchers({ nested: (slug) => slug.length >= 2, }); export default docsSlugNested.createPage(({ slug }) => { return <article>{slug.join("/")}</article>; });

ここで/docs/introはフォールスルーし(長さ 1)、/docs/guide/installはマッチします。

型付きエイリアス。 createMatchersに渡せるエイリアスは、型生成時にルートパスから抽出されます。[id=numeric]のようなパスは{ numeric?: (value: string) => boolean }を生成し、[...slug=nested]のようなパスは{ nested?: (value: string[]) => boolean }を生成します。パスに存在しないエイリアスを使用するのは型エラーです。

マッチャーとバリデーションの違い。 これらは異なる問題を解決します。

複数の兄弟ルートが同じパラメータスロットを共有しつつ、その形状が異なる場合はマッチャーを使用してください。単一のルートがレンダリング前に抽出済みのパラメータを制約する必要がある場合はバリデーションを使用してください。

スコープ。 マッチャーはページ、アウトレットページ、ミドルウェア、および API ルートハンドラーで尊重されます。レイアウトとエラー/ローディング/フォールバックの境界はマッチャーディスパッチに参加しません — それらは包含関係とラップするページによって選択されます。

ファイルシステムベースのルーターを使用すると、ランタイムは仮想的な@lazarv/react-server/routesモジュールを自動的に生成します。このモジュールはアプリケーション内のすべてのルートに対して名前付きルートディスクリプタをエクスポートします — ファイル構造から導出され、開発中はリアルタイムで更新され、各ビルドで再生成されます。

import { index, about, user, dashboard } from "@lazarv/react-server/routes";

各エクスポートされたディスクリプタは、createRouteで作成されたものと同じ機能を持つ完全なRouteDescriptorです:

さらに、ページコンポーネントの型付けのためのコンテキスト対応ヘルパー関数:

これらのヘルパーはランタイムではアイデンティティ関数です — コンポーネントをそのまま返します。ルートパス、validateエクスポート、アウトレット構造から正しいプロップ型を推論するためにTypeScript専用で存在します。

@lazarv/react-server/routesと並んで、ファイルルーター内のすべてのアウトレットに対して、型付きでサーバープリロードされるバインド済みのReactServerComponentを公開する仮想モジュール@lazarv/react-server/outletsも生成されます。詳細は下の型付きアウトレットコンポーネントを参照してください。

ルート名はファイルパスからキャメルケースで自動的に導出されます:

ファイルパスエクスポート名
pages/page.tsx or pages/index.tsxindex
pages/about.tsxabout
pages/user/[id].page.tsxuser
pages/user/posts.page.tsxuserPosts
pages/dashboard/settings.page.tsxdashboardSettings
pages/product/[sku=uppercase].page.tsxproductSkuUppercase
pages/docs/[...slug=nested].page.tsxdocsSlugNested

[id]のような動的セグメントは名前から除去されます。pages/[id].page.tsxのような純粋に動的なパスでは、パラメータ名(id)がルート名として使用されます。セグメントがマッチャーエイリアス(例:[sku=uppercase])を持つ場合、パラメータ名とエイリアスの両方が導出名に寄与します — そのため、マッチャーでゲートされたページとそのベアな兄弟は、数値サフィックスなしで自動的に別々の名前を取得します。数値サフィックスは、導出が真の衝突を生み出した場合の最後の手段としてのみ適用されます。

ページファイルからroute定数をエクスポートすることで、自動導出された名前をオーバーライドできます:

export const route = "userProfile";

エクスポートされる名前は自動導出された名前の代わりにuserProfileになります:

import { userProfile } from "@lazarv/react-server/routes";

createPageを使用して、ルートのパスパラメータとvalidateエクスポートから推論された型付きプロップを取得します:

pages/user/[id].page.tsx
import { 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.tsx
import { 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.tsx
import { 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として型付けされます。

前のセクションでは受け取り側 — 隣接する@outletName/ディレクトリからファイルルーターが埋めるアウトレットスロットを消費するレイアウト — を示しました。送り出し側にも独自の型付きエントリーポイントがあります:ファイルルーターで宣言されたアウトレットごとに1つの名前空間をエクスポートする、仮想的な@lazarv/react-server/outletsモジュールです。

import { sidebar, content } from "@lazarv/react-server/outlets"; <sidebar.Outlet url="/dashboard/nav" /> <content.Outlet url="/dashboard/feed" />

各名前空間は、そのアウトレット名にバインドされた1つのOutletコンポーネント(PascalCase、JSX呼び出し可能)を公開します。背後にある<ReactServerComponent outlet="sidebar" url="…" />形式と比較して、これは3つの利点をもたらします:

デフォルトではアウトレットはサーバー側でプリロードされます:バインドされたコンポーネントがサーバーコンポーネント内でレンダリングされると、ランタイムはurlをファイルルーターのマニフェストに対して解決し、対応する@outletName/...page.tsx(または@outletName.default.tsxフォールバック)を見つけ、それをレンダリングして結果をchildrenとしてReactServerComponentに渡します。そのため、SSRのHTMLには初回描画の時点でアウトレットコンテンツが含まれており、クライアントのラウンドトリップは不要です。

2つのオプトアウト:

pages/panels.page.tsx
import { sidebar, content } from "@lazarv/react-server/outlets"; export default function Panels() { return ( <div style={{ display: "grid", gridTemplateColumns: "200px 1fr" }}> <aside> {/* /dashboard/nav をサーバー側でサイドバースロットに解決します */} <sidebar.Outlet url="/dashboard/nav" /> </aside> <main> {/* `defer` でハイドレーション後のクライアントフェッチに切り替えます */} <content.Outlet url="/dashboard/feed" defer /> </main> </div> ); }

ランタイムは.react-serverディレクトリのreact-server-routes.d.tsの隣にreact-server-outlets.d.tsを書き出します。このファイルは@lazarv/react-server/outletsモジュールを宣言し、マニフェストで検出された各アウトレット名に対して1つの名前空間を持ちます。有効なJavaScript識別子ではないアウトレット名(例:ハイフン付きのディレクトリ名)はこのモジュールから除外されます — そのようなアウトレットは引き続き文字列指定の<ReactServerComponent outlet="..." />形式で到達可能です。

ローディングとエラーコンポーネントも同じパターンに従います:

pages/user/[id].loading.tsx
import { user } from "@lazarv/react-server/routes"; export default user.createLoading(() => { return <div>Loading user profile...</div>; });
pages/user/[id].error.tsx
import { 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.tsx
import { 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> ); });

動的セグメントのないルート(indexaboutなど)は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(createRoutecreateRouter、スキーマバリデーション戦略、パース関数など)については、型付きルーターページを参照してください。

TypeScriptでファイルシステムベースのルーターを使用すると、ランタイムはファイル構造から型定義を自動的に生成します。これらの型は以下をカバーします:

型チェックを有効にするには、生成された型を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(createRoutecreateRouter、型付きフック、スキーマバリデーションなど)については、型付きルーターを参照してください。