Next.jsのModalルーティング設計(App Router編)

〜履歴と状態を両立させる実践的UI設計〜

はじめに

Next.js 13以降で導入されたApp Routerは、ページやレイアウトを柔軟に構成できるようになりました。
その中でも「モーダル(ダイアログ)」をURLルーティングで制御するパターンは、複雑なUIをシンプルに保つ上で非常に有効です。

本記事では、App Routerでモーダルをルーティングとして扱う設計方法を紹介します。

モーダルをルーティングで表現する考え方

従来のモーダルは「状態管理(useStateなど)」で開閉していましたが、これだと履歴が扱いづらくなります。
App Routerでは、URL構造でモーダルの状態を表すのがベストプラクティスです。

例:

/users → 一覧ページ  
/users/new → 新規作成モーダル  
/users/[id] → 詳細モーダル

これにより:

というメリットがあります。

ディレクトリ構成例

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>
  );
}

ポイント:

一覧ページでモーダルを開くリンクを設置

// 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の並列ルートを活用することで、
モーダルを「状態」ではなく「ルーティング」として扱う設計が可能になります。

これにより:

といったメリットが得られます。

次回は、環境変数と設定管理を型安全に行う方法について紹介します。