Next.jsのModalルーティング設計(App Router編)
〜履歴と状態を両立させる実践的UI設計〜
はじめに
Next.js 13以降で導入されたApp Routerは、ページやレイアウトを柔軟に構成できるようになりました。
その中でも「モーダル(ダイアログ)」をURLルーティングで制御するパターンは、複雑なUIをシンプルに保つ上で非常に有効です。
本記事では、App Routerでモーダルをルーティングとして扱う設計方法を紹介します。
モーダルをルーティングで表現する考え方
従来のモーダルは「状態管理(useStateなど)」で開閉していましたが、これだと履歴が扱いづらくなります。
App Routerでは、URL構造でモーダルの状態を表すのがベストプラクティスです。
例:
/users → 一覧ページ
/users/new → 新規作成モーダル
/users/[id] → 詳細モーダル
これにより:
- モーダルを閉じてもURLが戻る(
router.back()でOK) - ページリロードしても同じモーダルが再現される
- ブラウザの履歴とも整合性が取れる
というメリットがあります。
ディレクトリ構成例
app/
users/
page.tsx # 一覧ページ
@modal/
new/
page.tsx # 新規作成モーダル
[id]/
page.tsx # 詳細モーダル
@modal は「並列ルート(Parallel Routes)」機能を利用しており、メインコンテンツとモーダルを同時に描画できます。
レイアウトでモーダルを受け取る
次に、モーダルを受け取る親レイアウトを用意します。
// app/users/layout.tsx
import type { ReactNode } from "react";
export default function UsersLayout({
children,
modal,
}: {
children: ReactNode;
modal: ReactNode;
}) {
return (
<>
{children}
{modal}
</>
);
}
このレイアウトにより、/users と /users/@modal/... が同時に描画されます。
モーダルはmodalスロットとして自動的に注入されます。
モーダルのページ実装
// app/users/@modal/new/page.tsx
"use client";
import { useRouter } from "next/navigation";
export default function NewUserModal() {
const router = useRouter();
return (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center">
<div className="bg-white p-6 rounded-lg shadow-xl w-96">
<h2 className="text-xl font-bold mb-4">新規ユーザー作成</h2>
<form className="flex flex-col gap-2">
<input type="text" placeholder="名前" className="border p-2" />
<input type="email" placeholder="メール" className="border p-2" />
<div className="flex justify-end gap-2 mt-4">
<button
type="button"
onClick={() => router.back()}
className="px-4 py-2 bg-gray-200 rounded"
>
キャンセル
</button>
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded">
登録
</button>
</div>
</form>
</div>
</div>
);
}
ポイント:
router.back()でモーダルを閉じる(履歴管理が自動)fixedでオーバーレイとして表示- サーバー遷移ではなくクライアント遷移のままモーダルを出す
一覧ページでモーダルを開くリンクを設置
// app/users/page.tsx
import Link from "next/link";
export default async function UsersPage() {
const users = [
{ id: "1", name: "田中 太郎" },
{ id: "2", name: "佐藤 花子" },
];
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">ユーザー一覧</h1>
<Link
href="/users/new"
className="bg-blue-600 text-white px-3 py-1 rounded"
>
+ 新規ユーザー
</Link>
<ul className="mt-4">
{users.map((u) => (
<li key={u.id}>
<Link href={`/users/${u.id}`} className="text-blue-600 underline">
{u.name}
</Link>
</li>
))}
</ul>
</div>
);
}
モーダル遷移も通常の<Link>で実現できるため、余分な状態管理は不要です。
注意点:モーダルのアニメーションと再描画
並列ルートでは、モーダルを開くたびにReactツリーが再生成されるため、
モーダルのアニメーションや入力中データを保持したい場合はClient Component内で状態を維持する工夫が必要です。
例:<AnimatePresence>(Framer Motion)を利用して、モーダルの開閉アニメーションを制御できます。
まとめ
App Routerの並列ルートを活用することで、
モーダルを「状態」ではなく「ルーティング」として扱う設計が可能になります。
これにより:
- URLでモーダル状態を再現できる
- 履歴管理が自然に動作する
- コードの見通しが良くなる
といったメリットが得られます。
次回は、環境変数と設定管理を型安全に行う方法について紹介します。