🗺️

React Router v7 の内部構造を探る:リクエストからレンダリングまでの道のり

2025/03/29に公開

はじめに

React Router は、React アプリケーションにおけるルーティングライブラリのデファクトスタンダードとして長年利用されてきました。v6 で Data API が導入され、フルスタックフレームワークとしての側面が強化されましたが、v7 ではさらに進化し、Vite との統合、Single Fetch、Lazy Loading といったモダンな機能がデフォルトで組み込まれ、より洗練された開発体験とパフォーマンスを提供します。

しかし、これらの機能がどのように連携し、ブラウザのリクエストがどのように処理され、最終的にページが表示されるのか、その内部構造は少し複雑に見えるかもしれません。

この記事では、React Router v7 で構築されたアプリケーションの動作フローを、主要なパッケージやコンポーネントの役割、データ取得の仕組み、レンダリングプロセスなどに焦点を当てて、内部構造の観点から解説します。具体的なコード例よりも、全体の流れと重要な概念の理解を目的とし、適宜 GitHub のソースコードへのリンクを付記します。

対象読者:

  • React Router v6/v7 をある程度利用したことがある開発者
  • React Router の内部的な仕組みやデータフローに興味がある方
  • SSR/CSR、Single Fetch、Lazy Loading といった概念がどのように実装されているか知りたい方

パッケージ構成の概観

React Router v7 はモノレポ構成を採用しており、機能ごとに複数のパッケージに分割されています。主要なパッケージとその役割を理解することが、全体の流れを把握する第一歩となります。

  • react-router: (packages/react-router)
    • React Router のコアロジックを提供します。
    • <Routes>, <Route>, <Outlet>, <Navigate> といった基本的なコンポーネント。
    • useLocation, useParams, useNavigate, useRoutes, useLoaderData, useActionData などのコアフック。
    • プラットフォーム非依存のルーターカーネル (@remix-run/router から内部化)。
    • SSRやSingle Fetchに関連するコンポーネント (<Meta>, <Links>, <Scripts> など) もv7からこのパッケージに含まれます。
  • react-router-dom: (packages/react-router-dom)
    • DOM 環境(Web ブラウザ)向けの API を提供します。
    • <BrowserRouter>, <HashRouter>, <Link>, <NavLink> など。
    • v7 では、その多くの機能が react-router 本体に統合され、主に後方互換性やDOM特化のAPI(<RouterProvider>ReactDOM.flushSync 利用など)を提供します。実質的には react-router を再エクスポートする部分が大きいです。
  • @react-router/dev: (packages/react-router-dev)
  • @react-router/create-react-router: (packages/create-react-router)
    • npm create react-router コマンドの実体で、プロジェクトの雛形を生成します。
  • @react-router/node, @react-router/express, @react-router/cloudflare etc.:
    • 各サーバープラットフォーム用の アダプター を提供します。
    • プラットフォーム固有の Request/Response オブジェクトを標準の Web API に変換したり、プラットフォーム固有の機能(セッションストレージなど)を提供したりします。
    • createRequestHandler をエクスポートし、サーバーフレームワークとの連携を担います。 (例: packages/react-router-express/server.ts#createRequestHandler)
  • @react-router/serve: (packages/react-router-serve)
    • 本番環境用のシンプルな Node.js アプリケーションサーバー。@react-router/express を内部で使用しています。
  • @react-router/fs-routes: (packages/react-router-fs-routes)
    • ファイルシステムベースのルーティング規約(Remix v2 形式)を提供します。routes.ts 内で使用できます。
  • @react-router/remix-routes-option-adapter: (packages/react-router-remix-routes-option-adapter)
    • 従来の Remix の remix.config.js 内の routes オプション形式を routes.ts で使用するためのアダプター。

起動から初回表示までの道のり (SSRフロー)

ユーザーが最初にページにアクセスした際の、サーバーサイドでの処理の流れを見ていきましょう。

  1. [サーバー] リクエスト受信とアダプター

    • ブラウザからのリクエストが Express などのサーバーに到着します。
    • @react-router/expresscreateRequestHandler で生成されたハンドラーが呼び出されます。
    • アダプターは Express の req オブジェクトなどから標準の Request オブジェクトを作成します。
  2. [サーバー] ルートマッチングとデータ取得 (Core Handler)

  3. [サーバー] サーバーサイドレンダリング (SSR)

    • サーバーエントリーポイント (entry.server.tsxdefault export) が呼び出されます。
    • React の renderToPipeableStream (または環境に応じたAPI) が <ServerRouter> コンポーネントをレンダリングします。
    • <ServerRouter> は内部で createStaticRouter を使い、StaticHandlerContext から受け取ったデータで初期化されたルーターインスタンスを作成します。
    • React コンポーネントツリー (root.tsx から <Outlet> を通じて) がレンダリングされます。
    • <Meta />, <Links />, <Scripts /> が適切なタグをHTMLに挿入します。
      • <Scripts /> は、ハイドレーションに必要なデータ (loaderData, actionData, errors) を window.__reactRouterContext としてインライン <script> にシリアライズし、クライアントJSのエントリーポイントを読み込む <script type="module"> も生成します。
      • (関連コード: packages/react-router/lib/dom/ssr/components.tsx#Scripts)
  4. [サーバー] レスポンス送信

    • レンダリングされたHTMLストリームと、収集されたヘッダー(Set-Cookie など)を含む Response オブジェクトが構築されます。
    • プラットフォームアダプターがこの Response をサーバーフレームワークのレスポンスに変換してクライアントに送信します。

クライアントサイドの魔法: ハイドレーション

サーバーから送られてきたHTMLを、ブラウザ上でインタラクティブなReactアプリケーションとして復元するプロセスです。

  1. [ブラウザ] アセット受信と解析:
    • ブラウザはHTMLを受信し、DOMツリーを構築します。<link> タグによりCSSやJSモジュールのダウンロードが始まります。
  2. [ブラウザ] クライアントエントリー実行:
    • <Scripts /> が出力した <script type="module"> により entry.client.tsx が実行されます。
  3. [ブラウザ/React] ハイドレーション:
    • hydrateRoot が呼び出されます。
    • <HydratedRouter> (または <RouterProvider>) がレンダリングされます。
    • 内部で createHydratedRouter が呼び出され、window.__reactRouterContext 等からサーバーの状態 (loaderData, errors など) を読み取ります。Single Fetch のストリームデータもこのタイミングでデコードされ始めます。
    • createBrowserRouter (または createHashRouter) を使ってクライアントルーターが初期化されます。
    • React はサーバー生成のHTMLとクライアント生成のコンポーネントツリーを比較し、イベントリスナーをアタッチしてDOMをインタラクティブにします。
  4. [ブラウザ] 完了:
    • ハイドレーションが完了し、アプリケーションが操作可能になります。useEffect などが実行され、必要に応じて <ScrollRestoration> がスクロール位置を調整します。

ページ遷移の裏側 (クライアントサイドナビゲーション)

ハイドレーション後、ユーザーが <Link> をクリックした際の動作です。

  1. <Link> クリックとイベント抑制:
  2. router.navigate():
    • <Link>router.navigate() を呼び出し、React Routerにナビゲーションの開始を伝えます。
  3. 状態更新とHistory API:
    • ルーターは state.navigation'loading' に更新します。
    • History API (pushState または replaceState) を操作してブラウザのURLを更新します。
  4. ルートマッチングとLazy Loading:
  5. Single Fetch (Loader):
  6. 状態更新とUIレンダリング:
    • Lazy Loadingとデータ取得が完了したら、state.loaderData 等が更新されます。
    • <RouterProvider> がトリガーされ、useRoutesImpl が新しいコンポーネントツリーを計算し、<Outlet> などが更新されます。
  7. スクロール復元:
    • <ScrollRestoration> が遷移前ページのスクロール位置を保存し、遷移先ページのスクロール位置を復元(またはリセット)します。
  8. ナビゲーション完了:
    • state.navigation'idle' に戻ります。

データの変更 (Form Submission / Action)

<Form> 送信時の流れはナビゲーションと似ていますが、Actionの実行とそれに続くRevalidationが含まれます。

  1. <Form> サブミットとイベント抑制:
    • サブミットイベントが発生し、event.preventDefault() でデフォルト動作をキャンセルします。
  2. router.navigate():
    • フォームの情報(action, method, formDataなど)を使って router.navigate() が呼び出されます。
  3. 状態更新 (Submitting):
    • state.navigation'submitting' になり、フォームデータなどが格納されます。
  4. ルートマッチングとLazy Loading (Action):
    • Actionに対応するルートを特定し、必要なら lazy() を実行します。
  5. Single Fetch (Action):
  6. 状態更新とリダイレクト/Revalidation:
    • state.actionData が設定されます。
    • Actionがリダイレクトを返した場合、新しいナビゲーションが replace: true で開始されます。
    • リダイレクトがない場合、Revalidation がトリガーされます。関連する loader が再度 Single Fetch (GET) で呼び出されます。
  7. UIレンダリング、スクロール処理、完了:
    • ナビゲーションと同様に、UIが更新され、スクロールが処理され、state.navigation'idle' に戻ります。

キーとなる概念の深掘り

  • Vite統合 (@react-router/dev): ビルド時の最適化(コード分割)、開発時の高速な HMR、サーバーリクエストハンドリングの仲介など、React Router をフレームワークとして機能させるための重要な役割を担います。
  • Single Fetch: ナビゲーションやサブミッション時に発生する複数のデータ取得/更新リクエストを1つにまとめる仕組みです。これにより、ネットワークのオーバーヘッドを削減し、ウォーターフォール問題を緩和します。サーバー側とクライアント側で協調して動作します。
  • Lazy Loading: ルート定義時に lazy() 関数を指定することで、そのルートが実際に必要になるまで関連モジュールの読み込みを遅延させます。初期バンドルサイズを削減し、アプリケーションの起動時間を短縮します。
  • <RouterProvider> と状態管理: ルーターの現在の状態(location, loaderData, navigation stateなど)を一元管理し、コンテキストを通じて配下のコンポーネントに提供します。これにより、フック (useLocation, useLoaderData, useNavigationなど) を使って状態にアクセスできます。
  • アダプター: Express, Cloudflare Workers, Node.js httpサーバーなど、様々な実行環境の差異を吸収し、共通のインターフェースでReact Routerを利用可能にします。

まとめ

React Router v7 は、単なるルーティングライブラリを超え、データ取得、ミューテーション、レンダリング、開発ツールを統合したフルスタックに近いフレームワークへと進化しました。Vite との緊密な連携、Single Fetch による効率的なデータ通信、Lazy Loading によるパフォーマンス最適化などがその核となる特徴です。

この記事で解説した内部構造の流れを理解することで、アプリケーションの動作をより深く把握し、デバッグやパフォーマンスチューニングに役立てることができるでしょう。

参考資料:


免責事項: この記事は React Router v7.4.1 時点の情報に基づいており、内部実装は将来のバージョンで変更される可能性があります。GitHub のリンクは特定のコミットハッシュ (252d928...) を指していますが、最新の情報はリポジトリの最新状態を確認してください。

GitHubで編集を提案

Discussion