【React Router v6】Route をネストしたときの動きを確認する【Next.js から移植】

:::note info
本記事では分かりやすさを重視するため、F1 を題材として取り扱っています。
:::

https://www.formula1.com/en/teams.html

# はじめに

Next.js のルーティングについて調べていたら、どうやら React Router がバージョンアップしてるらしいぞという情報を得たので触ってみました。

https://qiita.com/naoki-funawatari/items/dc6667b63f7ba5bf4a79

せっかくなので Next.js で作ったアプリを React に移植する形で進めていこうと思います。

https://github.com/naoki-funawatari/test-next-routing

## React プロジェクトを作成

`create-react-app`コマンドを使ってプロジェクトを作成します。

```bash
yarn create react-app test-react-routing
```

今回は React でルーティングする際にマストで必要となる`react-router-dom`の他に、`craco`、`prettier`の計 3 つのパッケージをプロジェクトに追加しています。

```bash
yarn add craco prettier react-router-dom
```

## 準備

まず Next.js の機能を参照しているコンポーネントを純粋な React のものに置き換えていきます。
基本的にコンポーネントは切り出して別コンポーネントとして作成していたので、あまり変更箇所は多くないです。

### useParams.js

`useRouter`を`useLocation`に置き換えていきます。

```diff_javascript:useParams.js
-import { useRouter } from "next/router";
+import { useLocation } from "react-router-dom";

const useParams = () => {
-  const location = useLocation();
+  const location = useLocation();

  return {
-    teamId: location.state?.teamId ?? null,
-    driverId: location.state?.driverId ?? null,
+    teamId: location.state?.teamId ?? null,
+    driverId: location.state?.driverId ?? null,
  };
};

export { useParams };
```

### Image.jsx

`next/image`の`Image`コンポーネントを純粋な`img`タグに置き換えていきます。

```diff_jsx:Image.jsx
-import NextImage from "next/image";

const Image = ({ alt, children, ...props }) => (
-  <NextImage alt={alt} {...props}>
+  <img alt={alt} {...props}>
    {children}
-  </NextImage>
+  </img>
);

export default Image;
```

### Link.jsx

`next/link`の`Link`コンポーネントを`react-router-dom`の`Link`コンポーネントに置き換えていきます。

```diff_jsx:Link.jsx
-import NextLink from "next/link";
+import { Link as ReactLink } from "react-router-dom";

-const Link = ({ children, ...props }) => <NextLink {...props}>{children}</NextLink>;
+const Link = ({ children, ...props }) => <ReactLink {...props}>{children}</ReactLink>;

export default Link;
```

### Nav.jsx

あとは呼び出し箇所の変更を行います。
`useParams.js`と`Image.jsx`の使い方は特に変わりませんが、`Link.jsx`は Next.js と React Router で少し異なります。

- Next.js の場合  
→ `href`にクエリ文字列を含む遷移先の URL、`as`に表示上の URL を設定する。
- React Router の場合  
→ `to`に遷移先の URL、`state`にオブジェクト形式でパラメータを設定する。

といった具合です。別に大したことではないんですが。

```diff_javascript:Nav.jsx
-const asUrl = `/${team.teamName}/${driver.driverName}`.replace(/\s/g, "-");
-const url = `/[team]/[driver]?teamId=${team.teamId}&driverId=${driver.driverId}`;
+const url = `/${team.teamName}/${driver.driverName}`.replace(/\s/g, "-");
+const state = { teamId: team.teamId, driverId: driver.driverId };

return (
  <li key={url}>
-    <Link href={url} as={asUrl}>
+    <Link to={url} state={state}>
      {driver.driverName}
    </Link>
  </li>
);
```

あとはアプリケーション全体を Rouer で囲ってしまえば準備完了で
す。

```diff_javascript:index.js
ReactDOM.render(
  <React.StrictMode>
+    <BrowserRouter>
      <App />
+    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById("root")
);
```

とりあえずここまでやれば React としては問題なく動くようになると思います。

## いざルーティング

と行きたいところだったんですが、**何もしていないのに想定通り**動いてしまっています。
これはある意味想定外です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213286/b99bf306-1920-63a5-191e-4381a35c5bcd.gif" width="200px" />

### なぜか

理由としては簡単で、ルーティング云々とは全く無関係に「あれば表示する」という作り方をしていたからでした。

```jsx:Main.jsx
<main>
  {team && <Team {...{ team }} />}
  {driver && (
    <>
      <hr />
      <Driver {...{ driver }} />
    </>
  )}
</main>
```

### ルーティングの実装

気を取り直してルーティング処理を実装していきたいと思います。
React Router v6 からネストされたルートの記述がシンプルになっているということで、素直に実装していきます。

https://reactrouter.com/docs/en/v6/getting-started/overview#nested-routes

以下のようになるんじゃないかと思います。

```jsx:Main.jsx
<main>
  <Routes>
    <Route path=":team" element={<Team {...{ team }} />}>
      <Route
        path=":driver"
        element={
          <>
            <hr />
            <Driver {...{ driver }} />
          </>
        }
      />
    </Route>
  </Routes>
</main>
```

「よしこれで完成」と思いきや、あと一つ実装しなければいけないものがありました。

### Outlet とかいうヤツ

なんか新しいコンポーネントが追加されてるな、位にしか考えていなかったんですが、これが非常に重要でした。

https://reactrouter.com/docs/en/v6/api#outlet

Outlet が設定されていない場合、最初にマッチしたパスの要素しかレンダリングしてくれません。
今回の場合、

- `:team`
- `:team/:driver`

という 2 パターンの URL にマッチさせたいにも関わらず、`:team`にマッチした時点で以降のパスはどうやら無視されてしまうようです。

以下のようにチームのみ画面に表示されています
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213286/f71bb762-2474-060c-7714-5e70f8c368e8.gif" width="200px" />

Outlet を使用することでこれを回避することができます。
公式ドキュメントには以下のように記されています。
> An &lt;Outlet&gt; should be used in parent route elements to render their child route elements. This allows nested UI to show up when child routes are rendered. If the parent route matched exactly, it will render a child index route or nothing if there is no index route.

翻訳すると以下のようになります。
> &lt;Outlet&gt;は、子のルート要素をレンダリングするために、親のルート要素で使用する必要があります。これにより、子ルートがレンダリングされたときにネストされたUIが表示されるようになります。親ルートが正確にマッチした場合は子のインデックスルートをレンダリングし、インデックスルートがない場合は何もレンダリングしません。

powered by DeepL

https://www.deepl.com/translator

### Outlet を実装する

何も難しいことはありませんでした。
以下のように、親となる要素に対して`<Outlet />`と 1 行追加するだけです。

```diff_jsx:Team.jsx
+import { Outlet } from "react-router-dom";
import Image from "@/components/Image";
import chiefImage from "@/assets/images/chief.png";

const Team = ({ team }) => (
  <>
    <h1>{team.teamFullName}</h1>
    <h2>{team.teamChief}</h2>
    <Image src={chiefImage} alt="chief" />
+    <Outlet />
  </>
);

export default Team;
```

これで本当の意味で想定通りに動いている、といった状態になりました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213286/b99bf306-1920-63a5-191e-4381a35c5bcd.gif" width="200px" />

ソースコードは以下にあります。

https://github.com/naoki-funawatari/test-react-routing/tree/328d3b86fc672bdd6a7863f5148915b453ceedb2