はじめに
NextjsのApp Routerはapp配下のディレクトリ構造でルーティングを行い、そこに様々な状況に応じたUIをファイル名をもとに定義します。
ディレクトリ構造によるルーティングは簡単にいうと、app/直下においたファイルは/にアクセスしたときのUIとして、app/dashboard直下に置いたファイルは/dashboardにアクセスしたときのUIとして扱われます。
表示させたいメインのUIをpage、読み込み中のUIをloading、エラー時のUIをerror、全体のレイアウトのUIをlayoutという名前のファイルに対して定義します。これらを用いて構築されるページは以下のようなReactコンポーネントと同等の内容が描画されます(他にも紹介しなかった状態のUIを定義するファイル名は存在しますし、UIに関連しないが特別に扱われるファイル名も存在します)。
<Layout>
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Loading />}>
<Page />
<Suspense>
<ErrorBoundary>
</Layout>
Layoutはlayoutに定義したコンポーネント、Errorはerrorに定義したコンポーネント、Loadingはloadingに定義したコンポーネント、Pageはpageに定義したコンポーネントです。
Pageコンポーネントより外の部分はよりディレクトリが深いところで引き継がれます。
<OuterLayout>
<ErrorBoundary fallback={<OuterError />}>
<Suspense fallback={<OuterLoading />}>
<Layout>
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Loading />}>
<Page />
<Suspense>
<ErrorBoundary>
</Layout>
</Suspense>
</ErrorBoundary>
</Layout>
このようにディレクトリ構造とファイル名を活用するApp Routerですが、Parallel Routesと言われる機能があります。これを用いることで同じパスに対する挙動を異なるディレクトリで宣言することができます。
Paralle Routes
App Routerはディレクトリ構成に従ったルーティングをしますが、ディレクトリ名の形式によって特別な挙動をすることがあります。Paralle Routesはそのようなディレクトリ名を特別に扱うことで利用できる機能の1つです。
この機能はディレクトリ名を@から始まる名前にすることで有効になります。そのディレクトリに宣言したUIは1つ浅いディレクトリに定義されたlayoutで扱えます
app/@helloとapp/@worldにUIがそれぞれ宣言されていることを考えます。通常のルーティング通りであればそこ定義したUIは/@helloや/@worldにアクセスしたときに扱われますが、ディレクトリ名が@から始まっている場合はこのパスにアクセスできません。代わりにapp直下にあるlayoutファイルで以下のようにUIを呼び出せます。
export default function Layout(props: {
children: React.ReactNode
hello: React.ReactNode
world: React.ReactNode
}) {
return (
<html>
<body>
{props.children}
{props.hello}
{props.world}
</body>
</html>
)
}
app直下のlayoutがpropsとして通常のchildrenの他に/app/@helloと/app/@worldに宣言されたUIをhelloやworldから受け取リます。受け取るpropsの名前はディレクトリ名から@をとったものです。
Paralle Routesはapp/@hello/accountsのように内部でディレクトリを積み重ねてルーティングを構築できます。パスが/accountsの画面ではprops.helloの部分にapp/@hello/accountsで定義したUIが表示されます。
app/accountsディレクトリにファイルを置いて/accountsにアクセスしてもapp/@hello/accountsやapp/@world/accountsにUIが宣言されていない場合はapp直下のnot-foundファイルに書かれたUIかデフォルトの404ページが表示されます。ただし、NextjsのLinkなどのナビゲーション機能を用いて遷移した場合はその部分に直前まで表示していたUIを表示し続けます。
/accountsに直接アクセスしたときにUIを表示させたい場合はapp/@hello/accountsとapp/@world/accountsにUIを宣言するか、app/@helloとapp/@worldにdefaultファイルを宣言させる必要があります。
defaultファイルはParalle Routes内に定義されていないパスにアクセスしたときに返すデフォルトのUIを宣言するものです。すべてのルートを制御するのは難しいため、not-foundを返すようにしたいとき以外はdefaultファイルにUIを宣言しておくのが良いと考えています。
使い方
ページ内で異なる意味のUIを表示させたい場合に読み込みやエラーの状態を分割するためにParalle Routesを利用することが最も簡単なケースです。
export default function Layout({
accounts,
dashboard,
}: {
accounts: React.ReactNode
dashboard: React.ReactNode
}) {
retrurn (
<>
{accounts}
{dashboard}
</>
)
}
この例のようにすることで、アカウントに関するUIとダッシュボードに表示するUIを分離して表示させます。
Paralle Routesはこの他にも条件によって異なるをUIを出し分ることが可能です。
export default function Layout({
children,
login,
}: {
children: React.ReactNode
login: React.ReactNode
}) {
const { isLoaded, userId } = useAuth();
if (!isLoaded || !userId) {
return login;
}
retrurn children
}
ログインしている時はユーザーに特化した内容を、ログインしていない場合はログインを促す画面やユーザーに依存していない画面を表示させます。
ログインユーザーに対するルーティングを行うディレクトリとログインしていないユーザーに対するルーティングを行うディレクトリで分割できるところに利点があります。
さらにIntercepting RoutesとParalle Routerを組み合わせて使う方法も有名です。
Intercepting Routesについての詳細は紹介しませんが、この機能も特別なディレクトリ名を付けることによって有効になります。(.)や(...)や(..)(..)のようなものが先頭についている場合に利用でき、ルーティングを先頭についた文字列をもとにハックします。
app/dashboard/(..)photosというディレクトリがあるとします。/dashboardから/photosにNextjsのLinkなどを用いた遷移を行った場合にapp/photosに宣言されたUIの代わりにapp/dashboard/(..)photosに宣言したUIを表示します。
こんなIntercepting RoutesはParalle Routerと組み合わせてある機能を作ることができます。
export default function PhotoModal({ photo }) {
return (
<Modal>
<Photo photo={photo} />
</Modal>
)
}
export default function Photo({ photo }) {
return <Photo photo={photo} />
}
export default function Layout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<>
{props.children}
{props.modal}
</>
)
}
export default function Photos() {
return (
<>
{photos.map((photo) => (
<Link key={photo.id} href="/photos/photo.id">
<Photo photo={photo} />
</Link>
)}
</>
)
}
app直下と@modal直下にはdefaultファイルが置かれており、nullを返すようになっているとします。
このように定義された時のルーティングは以下のようになります。
/にアクセスした時は、@modal側は返すものがないのでnullを返し、app/page.tsxに宣言されたUIからリンク付きのPhotoを複数表示させます。
Photoにあるリンクを使って/から/photo/:idにアクセスした時はapp/@modalに(..)photo/[id]が存在するので、app/photo/[id]のUIを表示せずに、app/@modal/(..)photo/[id]に宣言したUIを表示させます。
/photoに直接アクセスした時はapp/@modal側はnullを返し、app/photo/[id]に宣言したUIが表示されます。
このように組み合わせることで以下のサイトのような機能を作成できます。
Intercepting RoutesとParalle Routerを使うことで一覧から詳細へ訪れる時は背景で一覧をちらつかせつつモーダルを表示させ、別の場所や直接のアクセスがあった時は詳細をページ全体に表示させるようなルーティングを簡単に実装できると言うわけです。
おわりに
Paralle Routesを紹介しました。App Routerは直感的なルーティング機能が提供しています。そんな中で命名による特別なルーティングを行うと直感的な部分が損なわれてしますので使い過ぎには気をつけた方が良いです。
例えば、UIの意味によって分けるために利用すると、ルーティングの複雑さに対してメリットが小さいのでこの機能を利用せずにファイル内でSuspenseやErrorBoundaryなどの境界を引いて分離した方が良いと考えています。
しかし、ログインの有無によって表示させるコンテンツを切り替えるようなケースでは第2のディレクトリをParalle Routesを用いて記述することで綺麗に切り分けられるので有意義に利用できますし、最後に紹介したようなアクセスのされ方によって表現方法を変えるような実装はこれを使わなければより複雑な実装になってしまうと考えられるのでそのようなケースではParalle Routesは有効的と考えています。
便利な機能ですが、本当にParalle Routesが有効的か考えてから利用するように注意していきましょう!