本記事の内容
この記事では、Next.js の App Router における Intercepting Routes と Parallel Routes を使用したモーダル実装について解説します。
この機能を組み合わせることで、URLで共有可能なモーダルを実装できます。
実装するモーダル
Intercepting Routes とは
Intercepting Routes とは、特定のルートへのナビゲーションを「インターセプト(横取り)」して、異なるUIを表示する機能です。
インターセプトが発生する条件は、ソフトナビゲーションによる画面遷移で該当ページが表示されることです。
例) /parallel/modal
にアクセスした場合
・ソフトナビゲーション → app/parallel/@modal/(.)modal/page.tsx
が表示
・ハードナビゲーション → app/parallel/modal/page.tsx
が表示
インターセプトしたいページのフォルダには、先頭に特殊な記号をつけます。
-
(.)
- 同じレベルのセグメントと一致 -
(..)
- 1レベル上のセグメントと一致 -
(..)(..)
- 2レベル上のセグメントと一致 -
(...)
- appディレクトリのルートからのセグメントと一致
Intercepting Routes でできること
Intercepting Routes を使用することで、従来のモーダルでは実現が難しかった以下の機能を簡単に実現することができます
- モーダルコンテンツをURLで共有可能にする
- ページをリフレッシュしたときも、モーダルを閉じずにコンテキストを保持する
- 戻るナビゲーション時にモーダルを閉じる
- 前方ナビゲーション時にモーダルを再表示する
Parallel Routes とは
Parallel Routes とは、複数のルートにある page.tsx
を、1つのページに条件付きで同時にレンダリングできる機能です。
Slots
Parallel Routes を使用するには、「slots
」をファイル構成に導入します。
- フォルダ名の先頭に
@
をつける - 表示させたいページまでのセグメントを合わせた構造にする
-
layout.tsx
から使用する
slotsはURLの構造に影響を与えません。
例えば、/@parallel/views
の場合、@parallel
はスロットなので、URLは/views
になります。
layout.tsx
には children
と共に slots
が渡されます。
export default function Layout({
parallel,
children,
}: Readonly<{
folder: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<>
<section>{children}</section>
<section>{parallel}</section>
</>
);
}
Active State
Next.js
は各slot
について、表示状態を保持します。
Parallel Routes
では、ページが表示されていた状態で別の階層へソフトナビゲーションが起こった場合、そのURLに対応するページが存在しなくても、遷移前に表示していたページを表示し続けます。
ブラウザリロードなどのハードナビゲーションが発生すると、ページの状態が把握できなくなるため、対応するページが存在しないURLの場合、そのslotについてはdefault.tsx
の内容が表示されます。
何も表示したくない場合は、default.tsx
で null
を返すようにします。default.tsx
がないとエラーが発生します。
export default function Default(){
return null
}
実装例
ディレクトリ構造
app/
└── parallel/
├── layout.tsx # 親レイアウト
├── page.tsx # メインページ
├── modal/
│ └── page.tsx # ハードナビゲーション用モーダルページ
└── @modal/ # Parallel Routes用スロット
├── default.tsx # デフォルト表示(何も表示しない)
└── (.)modal/ # Intercepting Routes用フォルダ
└── page.tsx # モーダル表示用ページ
コード
app/parallel/layout.tsx
/*
`/parallel` 配下で共通利用されるレイアウト
Propsとして、各Slotを受け取る(Slotの表示制御はNext.jsのデフォルトの仕組みで行われる)
children: メインコンテンツ
modal: モーダルコンテンツ
*/
export default function ParallelLayout({
children,
modal
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Parallel Routes デモ</h1>
{children}
{modal}
</div>
);
}
app/parallel/page.tsx
/*
`/parallel` メインページ
*/
import Link from "next/link";
export default function ParallelPage() {
return (
<div className="p-4">
<h2 className="text-xl mb-4">メインページ</h2>
<Link
href="/parallel/modal"
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 inline-block"
>
モーダルを開く
</Link>
</div>
);
}
app/parallel/@modal/(.)modal/page.tsx
/*
ソフトナビゲーションで `/parallel/modal` にアクセスすると表示されるページ
ハードナビゲーションでは、`app/parallel/modal/page.tsx` が表示される
*/
'use client';
import { useRouter } from 'next/navigation';
export default function ModalPage() {
const router = useRouter();
return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg max-w-md w-full">
<h2 className="text-xl font-bold mb-4">モーダル</h2>
<p className="mb-4">これはParallel Routesを使用したモーダルです。</p>
<button
onClick={() => router.back()}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
閉じる
</button>
</div>
</div>
);
}
app/parallel/@modal/default.tsx
/*
Slotsに対応するページが存在しない場合、 return した内容が表示される
今回のケースでは return null としているので、何も表示しない。
*/
export default function Default() {
return null;
}
app/parallel/modal/page.tsx
/*
ハードナビゲーションで `/parallel/modal` にアクセスすると、表示されるページ
*/
export default function ModalContent() {
return (
<div className="p-4">
<h2 className="text-xl mb-4">モーダルコンテンツページ</h2>
<p>モーダルを開いた状態でフルリロードすると表示される</p>
</div>
);
}
まとめ
Intercepting Routes
と Parallel Routes
を活用したモーダル実装の経験がなく挑戦してみましたが、思いの外キャッチアップに時間がかかりました...
ナビゲーション方法によってユーザー体験が異なることに若干の違和感を覚えましたが、モーダルのstate管理が不要になるのは大きなメリットだと感じたので、積極的に活用していきたいと感じました🙋♂️
最近 X を始めたので、よければフォローしてください!
追記:
上記とは別の環境でモーダルを実装しようとしたところ、Issueと同様のエラーが発生しました。
一部バグが発生するケースがあるかもしれません。