React Routerは、Reactアプリでページ遷移を実現するライブラリの1つである。任意のURLセグメントとコンポーネントを対応づける形で、ルーティングを行う。

React Routerとは

React Routerには、Webアプリ用のreact-router-domと、ネイティブアプリ用のreact-rotuer-nativeという2種類のパッケージがある。

パッケージの種類 概要
react-router-dom Webアプリのためのルーティングを提供。
react-router-native デスクトップ/スマホアプリのためのルーティングを提供。

また、両者が共有するコアはreact-routerパッケージにまとめられている。

ほんで、react-routerとRemixが共有するコアはrouterというパッケージにまとめられている。

Remixについて

React Routerのチームは、RemixというReactベースのWebフレームワークを開発している。Remixは旧バージョンのReact Routerを土台としており、その開発過程で得られたルーティングのノウハウが新バージョン(V6)のReact Routerに反映されている。

前述の通り内部では@remix-run/routerというパッケージを使っており、おおよそは同じものだ。チュートリアル(React Router/Remix)に書かれている内容も共通する部分が多い。

これらのチュートリアルは、それぞれ「30-60分くらいで終わる」と見積もられている。信じて取り組んだら6時間くらい掛かった。泣いた。

本筋とは関係ないが、あれこれ調べていたらRemixってなんやねんという場面に何度か出会したので参考までに記しておく。

基本的な使い方

まずreact-router-domをプロジェクトにインストールする(Reactでの開発環境は整っているものとする)。

bash
npm install --save-dev react-router-dom

続いてコードを書く。最もシンプルなルーティングは、以下のような形を取る。

  1. createBrowserRouter()関数でRouterオブジェクトを作る。
  2. 1で作成したRouterオブジェクトを<RouterProvider>という要素のrouter属性に渡す。

これだけである。

jsx
import React from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/",
    element: <p>I'm the toooooop!</p>,
  },
  {
    path: "/child",
    element: <p>I'm your chiiiiiiiiiiiiiiiiiiild!</p>,
  },
]);

const rootDom = document.getElementById("root") as HTMLElement;
if (!rootDom) throw new Error("Root element not found");

const root = createRoot(rootDom);
root.render(
  <React.StrictMode>
    <a href="/">To Top</a>
    <RouterProvider router={router} />
    <a href="/child">To Child</a>
  </React.StrictMode>
);

終わりである。

To Childリンクをクリックすると、/childに遷移してI'm your chiiiiiiiiiiiiiiiiiiild!が表示される。

また、To Topリンクをクリックすると、/に遷移してI'm the toooooop!が表示される。遷移に応じて、ブラウザの進む、戻るもちゃんと機能する。

createBrowserRouter()でパスとReact要素を対応づけるRouterオブジェクトを作り、その情報をもとにRouterProviderコンポーネントがパスに応じたReact要素を表示していることがわかる。

上記のコードはエラーを出さず普通に動く。しかし、あんまりよろしくない点が2つある。

ルートにはRouterProviderだけを置く

まずアプリのルート(根っこの意味)には、<RouterProvider>を1つだけ置くことが推奨される。

例えばReactでは、アプリのルートをよくAppというコンポーネントで表す。<App>はただのReact要素であるのでどこにでも幾つでも配置できるが、通常はルートに1つだけ配置される。<App>がトップ以外にあったり、複数あったりしたら、何がどうグループ化されているのか理解に困るからだ。

<RouterProvider>も同様である。主に一貫性と保守性の観点から、アプリ全体のルーティングは一箇所で管理することが推奨される。つまり1つの<RouterProvider>ですべての要素の表示を振り分ける形が推奨される。

なお、<RouterProvider>に渡されるrouterオブジェクトは、一般にRoot routeと呼ばれるそうである。この根っこのrouteオブジェクトが、アプリのレイアウトを担うとも言える。

aタグではなくLinkタグを使う

先のサンプルでは、ページ遷移に<a>を使っている。これだと、ページ遷移のたびにドキュメント全体が更新される。どのブラウザでも、この仕様は変わらない。

SPAの大きな強みは、必要な箇所のみを更新することで実現する高速なレスポンスである。<a>ではこの強みが十分に発揮されない。

必要な箇所のみを更新するには、<a href="">の代わりに<Link to="">を使う必要がある。

修正版

2つの修正点を反映したものが以下のコードである。

jsx
import React from "react";
import { createRoot } from "react-dom/client";
import {
  createBrowserRouter,
  RouterProvider,
  Link,
  Outlet,
} from "react-router-dom";

// 共通レイアウトコンポーネントを新設
const Layout = () => (
  <>
    <Link to="/">To Top</Link>
    <Outlet /> {/* 子ルートのコンポーネントがレンダリングされる場所 */}
    <Link to="/child">To Child!</Link>
  </>
);

// トップページ用のコンポーネント
const TopPage = () => <p>I'm the toooooop!</p>;

// 子ページ用のコンポーネント
const ChildPage = () => <p>I'm a chiiiiiiiiiiiiiiiiiiilld!</p>;

// ルーターの定義
const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      {
        index: true,
        element: <TopPage />,
      },
      {
        path: "child",
        element: <ChildPage />,
      },
    ],
  },
]);

const rootDom = document.getElementById("root");
if (!rootDom) throw new Error("Root element not found");

const root = createRoot(rootDom);
root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

まずアプリ全体のレイアウトを担当するLayoutコンポーネントを新たに作った。この中で使われている<Outlet />タグは、そのパスに指定したchildrenプロパティの中身をレンダリングするためのタグである。<Outlet />がないと、children達に出番は来ない。

名前はLayoutでなくても構わないが、各ページの共通部分を切り出して階層化し、各階層の独自部分は<Outlet />タグで出力するのが、React Routerの基本的な使い方と言える。

例えば以下の指定では、/では<TopPage />と置き換わり、/childでは<ChildPage /><Outlet />と置き換わるイメージだ。

jsx
  {
    path: "/",
    element: <Layout />,
    children: [
      {
        index: true,//pathの代わりにこう指定すると、現在の階層の<Outlet/>にレンダリングされる
        element: <TopPage />,
      },
      {
        path: "child",
        element: <ChildPage />,
      },
    ],
  }

<Outlet />は複数箇所に配置できるが、elementプロパティに指定したものしか出力されない。<Outlet/>で出力できるのは、1つの階層に1種類と心得ておくべきである。

この設計の背景には、URLセグメントと表示コンポーネントが強く結びついているという事実がある(リンク先はチュートリアルで紹介されているサンプルである)。

URLの構造をアプリのコンポーネントの構造と対応させるのが、React Routerの本懐である。たぶん。

遷移によって大きく構造を変えたいときは、以下のように階層を避けてレンダリングするコンポーネントを振り分ければ良い。

jsx
const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    children: [
      /*略*/
    ],
  },
  {
    path: "child",
    element: <AnotherLayout />,
    children: [
      /*略*/
    ],
  },
]);

ルーティングに使う基本的な関数と要素

ここまでに使った関数と要素の詳細をまとめる。

createBrowserRouter()関数

createBrowserRouter()は、以下のように型宣言された関数である。

ts
function createBrowserRouter(
  routes: RouteObject[],
  opts?: {
    basename?: string; //アプリがルートにマウントされない場合のパス
    future?: FutureConfig; //v7の機能を使うかどうかのフラグのセット
    hydrationData?: HydrationState; //サーバサイドレンダリングで使う何か
    window?: Window; //グローバルwindow以外のwindowを使いたい時の何か
  }
): RemixRouter;

参考:createBrowserRouter v6.22.0 | React Router

入門レベルではrotuesだけ覚えておけば良さそうだ。

Routeオブジェクト

createBrowserRouterroutesプロパティに渡すのが、Routeオブジェクトの配列である。このオブジェクトは以下のように型宣言されている。

ts
interface RouteObject {
  //コンポーネントを表示するパスを指定
  //先頭のスラッシュはいらない
  path?: string;
  //現在の階層の<Outlet />に、このルート(Route)の要素/コンポーネントを表示するかを指定
  //これをtrueにするときはpathプロパティは指定しない
  //うまく説明できないが、先述のサンプルコードの通りである
  index?: boolean;
  //Routeオブジェクト単体か、その配列で子ルートを指定
  children?: React.ReactNode;

  //指定パスにアクセスした時にデータのロードを実行する関数を指定
  loader?: LoaderFunction;
  //フォーム送信などがあった場合に実行する関数を指定
  action?: ActionFunction;

  //指定パスに表示するReact要素
  element?: React.ReactNode | null;
  //指定パスに表示するコンポーネントを指定;
  Component?: React.ComponentType | null;

  //指定パスへの遷移時にエラーが発生した場合に表示するReact要素を指定
  //404で表示したい内容とか
  errorElement?: React.ReactNode | null;
  //指定パスへの遷移時にエラーが発生した場合に表示するコンポーネントを指定
  ErrorBoundary?: React.ComponentType | null;

  //SSRで、データのロード中に表示するReact要素を指定
  hydrateFallbackElement?: React.ReactNode | null;
  //SSRで、データのロード中に表示するコンポーネントを指定
  HydrateFallback?: React.ComponentType | null;

  //パスの大文字・小文字を区別するかを指定
  caseSensitive?: boolean;
  //ルートの識別子を指定
  id?: string;

  //ルート固有のデータを指定
  handle?: RouteObject["handle"];
  //ローダーでロードするデータの再検証をする関数を指定
  shouldRevalidate?: ShouldRevalidateFunction;
  //遅延ロードされるルートの定義を提供する関数を指定
  lazy?: LazyRouteFunction<RouteObject>;
}

参考:Route v6.22.0 | React Router

Routeタグ

頭文字が大文字になっていることからわかるように、RouteオブジェクトはReactコンポーネントである。オブジェクトではなく、<Route>タグで指定することもできる。

例えば先のサンプルは、以下のように書き換えられる。

jsx
import React from "react";
import { createRoot } from "react-dom/client";
import {
  createBrowserRouter,
  RouterProvider,
  Link,
  Outlet,
  Route, //追加
  createRoutesFromElements, //追加
} from "react-router-dom";

// 共通レイアウトコンポーネント
const Layout = () => (
  <>
    <Link to="/">To Top</Link>
    <Outlet />
    <Link to="child">To Child!</Link>
  </>
);

// トップページ用のコンポーネント
const TopPage = () => <p>I'm the toooooop!</p>;

// 子ページ用のコンポーネント
const ChildPage = () => <p>I'm a chiiiiiiiiiiiiiiiiiiilld!</p>;

// ルーターの定義
const router = createBrowserRouter(
  createRoutesFromElements(
    <Route path="/" element={<Layout />}>
      <Route index element={<TopPage />} />
      <Route path="child" element={<ChildPage />} />
    </Route>
  )
);

const rootDom = document.getElementById("root");
if (!rootDom) throw new Error("Root element not found");

const root = createRoot(rootDom);
root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

Routeオブジェクトのプロパティを、そのまま<Route>タグの属性に置き換えただけである。createRoutesFromElements()関数は、渡されたReact要素をもとにRouteオブジェクトを作って返す関数だ。

こっちの記法の方が宣言的でわかりやすい。

RouterProviderタグ

ts
declare function RouterProvider(props: RouterProviderProps): React.ReactElement;

interface RouterProviderProps {
  // サーバサイドレンダリングを利用しない場合、
  // 初期化のために各階層でローダーが実行される。
  // その間に表示する要素
  fallbackElement?: React.ReactNode;
  // createBrowserRouter()関数でこさえたRotuerオブジェクト
  router: Router;
  // v7の機能を使うかどうかのフラグのセット
  future?: Partial<FutureConfig>;
}

これもとりあえずはrouterだけ覚えておけば良さそうだ。

参考:RouterProvider v6.22.0 | React Router

Linkタグ

ts
declare function Link(props: LinkProps): React.ReactElement;

interface LinkProps
  extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
  //遷移先のパス
  //スラッシュをつけない相対パスで指定する
  //上の階層を指定する場合は、cdコマンドのように'.'を使う
  //例えば'../'で1つ上の階層を意味する
  to: To;
  //リンクをクリックして遷移する際に、ページの先頭へスクロールするのを防ぐか
  preventScrollReset?: boolean;
  //パスをRouteオブジェクトの階層で判断するか、パスの階層で判断するか
  //デフォルトでは'route'。
  relative?: "route" | "path";
  //遷移時にドキュメント全体をリロードするか
  //つまりaタグのように振る舞うか
  reloadDocument?: boolean;
  //履歴スタックの現在のエントリーを置き換えるか
  replace?: boolean;
  //history.stateに格納する値を指定
  state?: any;
  //よくわからん
  unstable_viewTransition?: boolean;
}

type To = string | Partial<Path>;

interface Path {
  pathname: string;
  search: string;
  hash: string;
}

参考:Link v6.22.0 | React Router

だいたい見たまんまである。

Outletタグ

Outletには何もなかろうと思ったらなんかあった。考えれば当然である。<Outlet />だけでは、親から子へ情報を受け渡せない。contextプロパティは、そのためにある。

子コンポーネントで値を受け取る場合は、useOutletContext()フックを使う。

ts
interface OutletProps {
  //親から子へ渡すデータ
  context?: unknown;
}
declare function Outlet(props: OutletProps): React.ReactElement | null;

参考:Outlet v6.22.0 | React Router

参考:useOutletContext v6.22.1 | React Router

React Routerのサンプルコード集

公式が用意したサンプルコード集がここにある。ドキュメントを見てわからない部分は、それっぽいサンプルを覗けば何とかなるかと思う。