0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Next.js】Intercepting Routes と Parallel Routes を活用したモーダル実装

Last updated at Posted at 2025-05-02

本記事の内容

この記事では、Next.js の App Router における Intercepting RoutesParallel Routes を使用したモーダル実装について解説します。

この機能を組み合わせることで、URLで共有可能なモーダルを実装できます。

実装するモーダル

モーダル非表示(/parallel
image.png

モーダル表示(/parallel/modal
image.png

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.tsxnull を返すようにします。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 RoutesParallel Routes を活用したモーダル実装の経験がなく挑戦してみましたが、思いの外キャッチアップに時間がかかりました...

ナビゲーション方法によってユーザー体験が異なることに若干の違和感を覚えましたが、モーダルのstate管理が不要になるのは大きなメリットだと感じたので、積極的に活用していきたいと感じました🙋‍♂️

最近 X を始めたので、よければフォローしてください!

追記:
上記とは別の環境でモーダルを実装しようとしたところ、Issueと同様のエラーが発生しました。

一部バグが発生するケースがあるかもしれません。

https://github.com/vercel/next.js/issues/72541

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?