本記事では分かりやすさを重視するため、F1 を題材として取り扱っています。
はじめに
Next.js のルーティングについて調べていたら、どうやら React Router がバージョンアップしてるらしいぞという情報を得たので触ってみました。
せっかくなので Next.js で作ったアプリを React に移植する形で進めていこうと思います。
React プロジェクトを作成
create-react-app
コマンドを使ってプロジェクトを作成します。
yarn create react-app test-react-routing
今回は React でルーティングする際にマストで必要となるreact-router-dom
の他に、craco
、prettier
の計 3 つのパッケージをプロジェクトに追加しています。
yarn add craco prettier react-router-dom
準備
まず Next.js の機能を参照しているコンポーネントを純粋な React のものに置き換えていきます。
基本的にコンポーネントは切り出して別コンポーネントとして作成していたので、あまり変更箇所は多くないです。
useParams.js
useRouter
をuseLocation
に置き換えていきます。
-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
タグに置き換えていきます。
-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
コンポーネントに置き換えていきます。
-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
にオブジェクト形式でパラメータを設定する。
といった具合です。別に大したことではないんですが。
-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 で囲ってしまえば準備完了で
す。
ReactDOM.render(
<React.StrictMode>
+ <BrowserRouter>
<App />
+ </BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);
とりあえずここまでやれば React としては問題なく動くようになると思います。
いざルーティング
と行きたいところだったんですが、何もしていないのに想定通り動いてしまっています。
これはある意味想定外です。
なぜか
理由としては簡単で、ルーティング云々とは全く無関係に「あれば表示する」という作り方をしていたからでした。
<main>
{team && <Team {...{ team }} />}
{driver && (
<>
<hr />
<Driver {...{ driver }} />
</>
)}
</main>
ルーティングの実装
気を取り直してルーティング処理を実装していきたいと思います。
React Router v6 からネストされたルートの記述がシンプルになっているということで、素直に実装していきます。
以下のようになるんじゃないかと思います。
<main>
<Routes>
<Route path=":team" element={<Team {...{ team }} />}>
<Route
path=":driver"
element={
<>
<hr />
<Driver {...{ driver }} />
</>
}
/>
</Route>
</Routes>
</main>
「よしこれで完成」と思いきや、あと一つ実装しなければいけないものがありました。
Outlet とかいうヤツ
なんか新しいコンポーネントが追加されてるな、位にしか考えていなかったんですが、これが非常に重要でした。
Outlet が設定されていない場合、最初にマッチしたパスの要素しかレンダリングしてくれません。
今回の場合、
:team
:team/:driver
という 2 パターンの URL にマッチさせたいにも関わらず、:team
にマッチした時点で以降のパスはどうやら無視されてしまうようです。
Outlet を使用することでこれを回避することができます。
公式ドキュメントには以下のように記されています。
An <Outlet> 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.
翻訳すると以下のようになります。
<Outlet>は、子のルート要素をレンダリングするために、親のルート要素で使用する必要があります。これにより、子ルートがレンダリングされたときにネストされたUIが表示されるようになります。親ルートが正確にマッチした場合は子のインデックスルートをレンダリングし、インデックスルートがない場合は何もレンダリングしません。
powered by DeepL
Outlet を実装する
何も難しいことはありませんでした。
以下のように、親となる要素に対して<Outlet />
と 1 行追加するだけです。
+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;
これで本当の意味で想定通りに動いている、といった状態になりました。
ソースコードは以下にあります。