App Router の Parallel Routes を完全理解する
Next.jsには強力なルーティング機能があります。App Routerにはさまざまなパターンの規約が用意されており、それぞれの特性を活かしてアプリケーションを構築できます。今回はその中でもParallel Routesに焦点を当てて探っていきたいと思います。
理解を深めるために、また読者が気になった点を自分でコードを変更させながら確かめられるという利点から、サンプルアプリを用意しました。サンプルアプリを実際に動かしながら記事を読んでいただくことで、この機能についてより一層の理解が深まると思いますので、ぜひお試しください。
今回使用するサンプルアプリは以下となります。
以下の手順で動作させることができます。
ローカルで動作させる手順
1.リポジトリのクローン
git clone https://github.com/s-hiraoku/next-parallel-interceping-routes.git
cd next-parallel-interceping-routes
2.依存関係のインストール
npm install
3.起動
npm run dev
4.ブラウザで起動
アプリが起動したらブラウザでhttp://localhost:3000にアクセスしてください。
Parallel Routes とは
Parallel Routesでは同じレイアウト内で複数のページを同時に、または条件付きでレンダリングすることがすることが可能です。ダッシュボードを例にとると以下のようにteamページとanalyticsページを並行してレンダリングすることができます。
用語
ここでParallel Routesを理解するうえで必要な用語を簡単に説明しておきます。
Segment
「Segment」とはスラッシュで区切られたURLパスの一部を指します。
例えば以下のようなURLがあるとします。
この場合、「dashboard」「users」「settings」がそれぞれ個別の「segment」に対応します。この構造に基づいて、ファイルシステム上でページやレイアウトを構成するディレクトリやファイルが配置されます。このURLを実現する場合、以下のようなディレクトリ構成などが考えられます。
Slot
Parallel Routesを使用する上で重要な役割を果たすのが「Slot」です。Slotは@folderという規約で定義されます。SlotはSegmentのように、その存在自体がURLに影響を与えることはありません。ディレクトリ構成は以下のようになります。
用語やルーティングの基礎について、もっと詳しい内容を知りたい方は、こちらの公式ベージを参照してください。
Parallel Routes を実現するレイアウト
Parallel Routesを実現するためのレイアウトを考えていきましょう。ディレクトリ構成は前述のSlotのものを利用したいと思います。
以下は、Parallel Routesに対応したレイアウト(app/dashboard/layout.tsx)です。
export default function DashboardLayout({
children,
team,
analytics,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{children}
{team}
{analytics}
</>
)
}
Slotは共有している親のレイアウトにPropsとして渡されます。このレイアウトのコンポーネントでは@analyticsと@teamのSlotのPropsを受け入れ、childrenのpropsと並行してレンダリングできます。
これらのSlotですが、常にレンダリングされるとは限りません。表示しているセグメントとSlot内でのセグメントが一致したときにだけ表示されます。
一致しなかった場合にはdefalut.tsxが表示されます。これは、SlotのフォールバックUIとなります。
また一度表示されたあとは、一致しないセグメントに移動しても、そのセクションは表示され続けます。ただしこれはソフトナビゲーションで画面遷移したときであって、ハードナビゲーションをしたときは表示されません。
ソフトナビゲーションとはクライアントサイドでのページ遷移を指します。この方式では、JavaScriptがブラウザ上で実行され、ページ全体のリロードを伴わずにコンテンツが更新されます。
ハードナビゲーションは、ブラウザ全体のページリロードを伴うナビゲーションです。サーバーに新たなリクエストを送信し、新しいHTMLドキュメントを取得して表示します。
試しにサンプルアプリで、両方のセクションを表示したあと、一致しないセグメントのURLでブラウザをリロードしてみてください。例えば、dashboard/analytics
でリロードすると以下のように表示されると思います。
teamの方が真っ白に表示されていますが、これはdefalut.tsxが表示されています。
Parallel Routes を使う
前章でParallel Routesの概要は理解できたかと思います。しかし、これをどういう時に使えばいいのかという疑問が湧いてきます。こういう場合はその機能の利点を理解すれば良いと思います。その利点を活かせる箇所に使えば良いという発想です。
Parallel Routesを使う利点としては、1つのレイアウトに複数のページをslotを使って表示できることです。コンポーネントを使うと同じようなレイアウトを作ることはできます。しかし以下の点でParallel Routesを使うメリットが出てくると筆者は考えています。順に説明していきたいと思います。
ルーティングとURL管理
Parallel Routes: 各セクション(analyticsやteam)に対して個別のルートを設定でき、URL自体がその状態を反映します。これによりユーザーは、他のセクションに状態や表示に影響を与えることなく、操作することができます。またアドレスバーのURLは表示が変更されるため、サイトのどこにいるかを把握しやすくなります。
アドレスバーのURLが変わっているところに注目してください。(小さくてすみません…。)
コンポーネント: コンポーネントを使用する場合、URLの管理は1つのルートに紐づいています。複数のコンポーネントをページ上に表示したい場合、URLのクエリパラメータや状態管理で表示するコンポーネントを切り替える必要があります。
この操作は状態管理を使ってコンポーネントを切り替えています。
アドレスバーのURLが変わっていないところに注目してください。
(ほんと小さくてすみません…。)
こういう操作はレイアウトが変わっているため、リンクで別ページに遷移させてあげるのがいいかと考えています。
アドレスバーのURLが変わっているところに注目してください。(🔎をお貸しします…。)
レンダリングとデータフェッチ
Parallel Routes: 各ルートに個別のレンダリングロジックが設定されるため、必要なコンポーネントのみをレンダリングします。また、特定のルートのみデータをフェッチすることで、他のコンポーネントに影響を与えずに効率的なデータフェッチが可能です。これにより、ユーザーの体験がスムーズになり、必要な部分のみ更新されるためパフォーマンスが向上します。
コンポーネント: ページの遷移やコンポーネントの切り替え時に、状態管理やプロパティの変更に基づいて必要なデータをフェッチすることが多いです。全体のリロードが必要な場合、すべてのコンポーネントが再レンダリングされることがあります。
状態管理とコンポーネントの独立性
Parallel Routes: 各ルートが独立しているため、各ルートごとに状態管理を行うことが可能です。これにより、あるルートでの変更が他のルートに影響を与えることなく、特定のルートだけを再レンダリングすることができます。
コンポーネント: 状態管理は全体的な状態やコンテキストで行われることが多く、特定の状態が変更された場合、関連するすべてのコンポーネントが再レンダリングされることがあります。
ユーザ体験からの観点
Parallel Routes: ユーザーが特定のページをブックマークしたり、URLを共有した場合でも、特定の状態やページが直接表示されます。また、ナビゲーションがシームレスになり、ユーザー体験が向上します。
コンポーネント: コンポーネント間の遷移時にページのリフレッシュや大きな状態の変化が発生することがあり、ユーザー体験が少しぎこちなくなる可能性があります。
ということで利点から使える場所を想像してみてください。敢えて言及はしませんが、コンポーネントで賄える機能はコンポーネントを使えばよさそうです。
サンプルアプリを修正してみよう
ということで、Parallel Routesを使って同じレイアウトに複数のページを表示する方法は、その利点を考えると、良い使い方だと言えそうです。
しかし、ブラウザをリロードすると、真っ白になるになってしまうのは問題があります。
まずは原因を探るため、defalut.tsxを見てみましょう。
export default function defaultPage() {
return null;
}
nullを返していますので、この挙動は当然ですね。
今回のアプリケーションでは、修正方法として以下の2点が考えられます。
- デフォルトのコンポーネントを設定する
- リダイレクトさせる
せっかく2つのSlotがあるので、両方を試してみましょう。analyticsの方をデフォルトのコンポーネントを設定し、teamの方をリダイレクトさせてみましょう。
それぞれのdefault.tsxを以下のように修正してみてください。
import { AnalyticsSection } from "@/app/_components/AnalyticsSection";
export default function defaultPage() {
return <AnalyticsSection />;
}
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
const DefaultComponent = () => {
const router = useRouter();
useEffect(() => {
router.replace("/dashboard/team");
}, [router]);
return <div>リダイレクト中...</div>;
};
export default DefaultComponent;
動作確認してみましょう。まずは、analyticsのdefalut.tsxから。
analyticsのdefault.tsxを確認するので、/dashboard/team
でブラウザをリロードします。
Analytics Section画面に遷移しました。
では次はteamのdefault.tsxを確認してみましょう。
Team画面に遷移しました。しかしそれと同時に今まで/dashboard/analytics
にいたのに、/dashboard/team
に遷移しています。当たり前ですけどね。そちらにリダイレクトしているのですから…。
これはユーザ体験としてはあまりよくなさそうです。リダイレクトするなら/dashboard
の方が良さそうですね。
他にもIntercepting Routes を使う方法でもできそうです。
Intercepting Routes については、また機会があれば記事を書こうと思います。
まとめ
Parallel Routesの動作ってどうなんだろって思って、公式サイトを見ました。しかし、公式サイトでは理解できない部分や記載されていない箇所があったため、サンプルアプリを作って確認してみました。思ったよりも面白かったので、この記事にまとめてみました。
App Routerの導入で、確かにディレクトリ設計やコンポーネント設計は複雑になってしまうのは否めず、メリットがわかりづらいという声も理解できます。「平和じゃなくなったなぁ」という意見にも一部賛同できます。でも良いところを見つけてあげて、そこをうまく使ってあげるとか、どうしても複雑になってしまう設計をうまく設計できるベストプラクティスを見つけるとか、なんか課題を与えられているようで筆者は楽しんでApp Routerを触っています。
(確かにプロダクトに採用を検討するとなると、検討に検討を重ね、我々にはそのような学習コストがあるかどうかなど…)
そういう形で平和が訪れたらいいですね。
参考資料
Discussion