言語
日本語
English

Caution

お使いのブラウザはJavaScriptが無効になっております。
当サイトでは検索などの処理にJavaScriptを使用しています。
より快適にご利用頂くため、JavaScriptを有効にしたうえで当サイトを閲覧することをお勧めいたします。

  1. トップページ
  2. Next.js辞典
  3. インターセプティングルート

インターセプティングルート

対応: Next.js 13(2022)

『Next.js』の App Router では、(.)(..) などの記法を使ったフォルダ名を定義することで、通常のページ遷移を横取り(インターセプト)して別のコンポーネントを表示できます。これをインターセプトルートと呼びます。典型的な使い方は、一覧ページ上でリンクをクリックしたときにフルページ遷移ではなくモーダルとして詳細を表示し、URLを正しく反映させながらも現在の画面を維持するという実装です。

記法一覧

記法概要
(.)同じ階層のセグメントをインターセプトします。
(..)1つ上の階層のセグメントをインターセプトします。
(..)(..)2つ上の階層のセグメントをインターセプトします。
(...)app ディレクトリのルート(最上位)からのセグメントをインターセプトします。

サンプルコード

写真一覧ページ(/photos)からリンクをクリックしたときに、/photos/[id] へのフルページ遷移をインターセプトしてモーダルとして表示する実装例です。URL は /photos/1 のように正しく変化しますが、画面は一覧のままモーダルが重なって表示されます。直接URLへアクセスした場合や、ページをリロードした場合は通常の詳細ページとして描画されます。

// ディレクトリ構成
// app/
// ├── @modal/                        // 並列ルートのモーダル用スロットです
// │   ├── (.)photos/[id]/
// │   │   └── page.js                // /photos/[id] をインターセプトします
// │   └── default.js                 // モーダルがないときの初期状態です
// ├── photos/
// │   ├── page.js                    // → /photos(写真一覧ページ)
// │   └── [id]/
// │       └── page.js                // → /photos/[id](写真詳細ページ)
// └── layout.js                      // @modal スロットを受け取るレイアウトです
// app/layout.js
// ルートレイアウトです
// @modal スロットを受け取り、通常コンテンツと並べて描画します

export default function RootLayout({ children, modal }) {
  return (
    <html lang="ja">
      <body>
        {/* 通常のページコンテンツです */}
        {children}

        {/* モーダルスロットです。インターセプト中はモーダルが入ります */}
        {modal}
      </body>
    </html>
  );
}
// app/photos/page.js
// 写真の一覧を表示するページです
// <Link> でクリックすると /photos/[id] へのナビゲーションが発生しますが、
// インターセプトルートによってモーダルが代わりに表示されます

import Link from 'next/link';

// サンプル用のダミーデータです
const photos = [
  { id: '1', title: '海の写真', src: '/img/sea.jpg' },
  { id: '2', title: '山の写真', src: '/img/mountain.jpg' },
  { id: '3', title: '街の写真', src: '/img/city.jpg' },
];

export default function PhotosPage() {
  return (
    <main>
      <h1>写真一覧</h1>
      <ul style={{ display: 'flex', gap: '16px', listStyle: 'none' }}>
        {photos.map(function(photo) {
          return (
            <li key={photo.id}>
              {/* クリックで /photos/1 などへ遷移します */}
              {/* インターセプトルートが有効なのでモーダルが開きます */}
              <Link href={'/photos/' + photo.id}>
                <img src={photo.src} alt={photo.title} width={200} height={150} />
                <p>{photo.title}</p>
              </Link>
            </li>
          );
        })}
      </ul>
    </main>
  );
}
// app/@modal/(.)photos/[id]/page.js
// /photos/[id] へのクライアントサイドナビゲーションをインターセプトして
// モーダルとして描画するコンポーネントです
// (.) は同じ階層(photos)のセグメントをインターセプトすることを示します

'use client';

import { useRouter } from 'next/navigation';

export default function PhotoModal({ params }) {
  // useRouter でモーダルを閉じる(前のページに戻る)操作を行います
  const router = useRouter();

  return (
    <div
      style={{
        position: 'fixed',
        inset: 0,
        background: 'rgba(0,0,0,0.7)',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        zIndex: 100,
      }}
      // オーバーレイをクリックするとモーダルを閉じます
      onClick={function() { router.back(); }}
    >
      <div
        style={{ background: '#fff', padding: '24px', borderRadius: '8px' }}
        // モーダル内部のクリックが外側に伝播しないようにします
        onClick={function(e) { e.stopPropagation(); }}
      >
        <h2>写真 ID: {params.id}</h2>
        <img
          src={'/img/photo-' + params.id + '.jpg'}
          alt={'写真' + params.id}
          width={600}
          height={400}
        />
        {/* 閉じるボタンで一覧ページに戻ります */}
        <button onClick={function() { router.back(); }}>閉じる</button>
      </div>
    </div>
  );
}
// app/photos/[id]/page.js
// URLに直接アクセスした場合やリロード時に表示される詳細ページです
// インターセプトルートはクライアントサイドナビゲーション時のみ有効なため、
// このファイルは必ず用意しておく必要があります

export default async function PhotoDetailPage({ params }) {
  // 実際のアプリではAPIやDBから写真データを取得します
  const photoId = params.id;

  return (
    <main style={{ padding: '40px' }}>
      <h1>写真詳細</h1>
      <img
        src={'/img/photo-' + photoId + '.jpg'}
        alt={'写真' + photoId}
        width={800}
        height={600}
      />
      <p>写真ID: {photoId}</p>
      {/* 一覧に戻るリンクです */}
      <a href="/photos">&larr; 一覧に戻る</a>
    </main>
  );
}
// app/@modal/default.js
// モーダルを表示しない通常状態のデフォルトファイルです
// このファイルがないとモーダルスロットが未定義になりエラーになります
// null を返すことで何も描画しないことを明示します

export default function ModalDefault() {
  return null;
}

概要

インターセプトルートは、(.) などの特殊な記法を使ったフォルダ名を定義することで機能します。クライアントサイドナビゲーション(<Link> クリックや router.push())によるルート遷移をインターセプトし、本来のページの代わりに別のコンポーネントを表示します。一方、URLへの直接アクセスやページのリロードではインターセプトが発生せず、通常のページが描画されます。この特性を利用することで、SNSのフィードから写真をモーダルで表示しつつURLを正しく共有できる、といった体験を実現できます。

インターセプトルートは、並列ルート(Parallel Routes)と組み合わせて使うことがほとんどです。上記の例では @modal という並列スロットを用意し、そこにインターセプトルートのコンポーネントを配置することで、一覧ページを維持したままモーダルを重ねて表示しています。

記法の階層は、ファイルシステムの深さではなく App Router のルートセグメントの階層を基準にします。(.) は同じセグメント階層、(..) は1つ上、(...)app ディレクトリのルートからを意味します。ルートグループの定義については ルートグループ もあわせてご参照ください。

よくあるミス

よくあるミス1: default.jsを置き忘れてモーダルスロットがエラーになる

並列ルートのスロット(例: @modal)を定義した場合、モーダルを表示しない通常状態でも何かを返すファイルが必要です。default.js がないと、インターセプトが発生していない状態でスロットの内容が未定義になりエラーになります。

ng_example
// NG: @modal スロットに default.js がないディレクトリ構成

// app/
// ├── @modal/
// │   └── (.)photos/[id]/
// │       └── page.js    // インターセプトルートのみで default.js がない
// ├── photos/
// │   └── page.js
// └── layout.js
Error: Missing default export in @modal slot.
The default export is required when using Parallel Routes.

app/@modal/default.js を作成し、null を返すデフォルトエクスポートを定義します。

ok_example.js
// app/@modal/default.js
// モーダルを表示しない通常状態のデフォルトファイルです
// null を返すことで何も描画しないことを明示します

export default function ModalDefault() {
  return null;
}

よくあるミス2: (.) と (..) の階層指定を間違えてインターセプトが効かない

インターセプトルートの記法はファイルシステムの深さではなく、App Router のルートセグメントの階層を基準にします。(.) は同じセグメント階層、(..) は1つ上を意味します。ファイルの配置場所を誤ると、インターセプトが発生せず通常のページ遷移になります。

ng_example
// NG: @modal は app 直下、インターセプト対象は app/photos/[id] だが
// (..) を使ってしまっている(実際には (.) が正しい)

// app/
// ├── @modal/
// │   └── (..)photos/[id]/   // 間違い: (..) は1つ上の階層を指す
// │       └── page.js
// └── photos/
//     ├── page.js
//     └── [id]/
//         └── page.js
インターセプトが発生せず、/photos/1 へのリンクをクリックすると
モーダルではなく通常の詳細ページに遷移してしまう。

@modalphotos が同じ階層(app 直下)にある場合は (.) を使います。

ok_example
// OK: @modal は app 直下、photos も app 直下なので (.) が正しい

// app/
// ├── @modal/
// │   └── (.)photos/[id]/    // 正しい: (.) は同じ階層のセグメントをインターセプトする
// │       └── page.js
// └── photos/
//     ├── page.js
//     └── [id]/
//         └── page.js

記事の間違いや著作権の侵害等ございましたらお手数ですがまでご連絡頂ければ幸いです。