Next.js 16 Parallel Routes と Intercepting Routes 完全ガイド:モーダル・ダッシュボード実装パターン

Next.js 16のParallel RoutesとIntercepting Routesを実装例で解説。モーダル・ダッシュボード・認証ゲートの設計、default.tsxの404回避まで網羅。

Next.js 16 Parallel Routes 実装ガイド 2026

更新日: 2026年5月26日

Next.js 16 の Parallel RoutesIntercepting Routes は、App Router 上で 1 つの URL に対して複数のページを同時にレンダリングしたり、ナビゲーションを途中で「横取り」して別のレイアウトで表示したりするためのファイルベースルーティング機能です。Parallel Routes はダッシュボードやタブ UI、独立ストリーミングに、Intercepting Routes は写真ギャラリーの「クリックでモーダル、URL は直接アクセス可能」というパターンに使われます。本記事では両者の違いと組み合わせ方を、実際に動く実装例とあわせて解説します。正直なところ、私自身も初めて触ったときは default.tsx 周りのハマりどころに 1 日溶かしたので、その辺りも具体的に書きます。

  • Parallel Routes は @folder 命名規則で「スロット」を定義し、同じレイアウト内に複数の独立したページツリーを並列レンダリングする機能
  • Intercepting Routes は (.) (..) (...) のマーカーで他の URL のレンダリングを途中で乗っ取り、現在のレイアウト内で表示する機能
  • 2 つを組み合わせると、Instagram のような「サムネクリックでモーダル表示、直リンクではフルページ」という UX が、わずか 4 ファイルで実装できる
  • スロットがマッチしない場合に表示する default.tsx の用意を忘れると、ソフトナビゲーション時に 404 が発生する
  • Parallel Routes の各スロットは独立して loading.tsx error.tsx を持てるため、ダッシュボードのウィジェット単位ストリーミングに最適
  • Next.js 16 では Turbopack の本番ビルド対応により、Intercepting Routes のコンパイル時間が従来比約 4 倍高速化

Parallel Routes とは何か:スロットの仕組み

Parallel Routes は、Next.js の App Router で 同じレイアウト内に複数のページを同時にレンダリングするためのファイル規約です。通常のルーティングでは 1 つの URL に対して 1 つのページコンポーネントがマウントされますが、Parallel Routes を使うとレイアウトコンポーネントが children 以外にも追加の Props を受け取り、それぞれが独立したルーティングツリーを持ちます。これらの追加スロットは @folder という命名規則で定義します。

例えば app/dashboard/ ディレクトリ配下に @analytics@team という 2 つのスロットを作成すると、dashboard/layout.tsx は次のような Props を自動的に受け取ります。

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <div className="grid grid-cols-2 gap-4">
      <section>{children}</section>
      <section>{analytics}</section>
      <section>{team}</section>
    </div>
  );
}

ここで重要なのは、各スロットが完全に独立した ルーティングサブツリーとして動作する点です。つまり @analytics 配下に loading.tsx error.tsx page.tsx を配置すれば、analytics スロットだけが独自のサスペンス境界とエラーバウンダリで動作します。これは Server Components の 公式ドキュメント でも「ウィジェット単位の独立したストリーミング」として推奨されているパターンです。

Parallel Routes の実装:ダッシュボード例

では、実際に管理ダッシュボードを Parallel Routes で構築してみます。ファイル構成は以下のとおりです。

app/
└── dashboard/
    ├── layout.tsx          # スロットを受け取るレイアウト
    ├── page.tsx            # children に対応するメインビュー
    ├── default.tsx         # children の default フォールバック
    ├── @analytics/
    │   ├── page.tsx        # /dashboard アクセス時のスロット
    │   ├── loading.tsx     # analytics 専用ローディング
    │   ├── error.tsx       # analytics 専用エラー
    │   └── default.tsx     # ソフトナビ時のフォールバック
    └── @team/
        ├── page.tsx
        ├── default.tsx
        └── members/
            └── page.tsx    # /dashboard/members 時に表示

ここで @analytics/page.tsx は重いデータベースクエリを実行するとします。React Server Components として実装すれば、analytics スロットだけが遅延しても、他のスロットはすぐ表示されます。

// app/dashboard/@analytics/page.tsx
import { fetchAnalytics } from "@/lib/db";

export default async function AnalyticsSlot() {
  // 2 秒かかる集計クエリ
  const data = await fetchAnalytics();
  return (
    <div>
      <h2>売上推移</h2>
      <p>今月: ¥{data.monthlyRevenue.toLocaleString()}</p>
    </div>
  );
}

// app/dashboard/@analytics/loading.tsx
export default function Loading() {
  return <div className="animate-pulse h-32 bg-gray-200" />;
}

この構成では、ユーザーが /dashboard を開いた瞬間に @teamchildren が即座に表示され、@analytics だけスケルトンが出てから 2 秒後に差し替わります。これを Cache Components と Partial Prerendering のガイド と組み合わせると、さらに静的シェルへのプリレンダリングまで両立できます。

Intercepting Routes の規約:(.) (..) (...) の違い

Intercepting Routes は、別のルートへのナビゲーションを 現在のレイアウトの中で「横取り」してレンダリングする機能です。ファイル名に特殊なマーカーをプレフィックスすることで動作し、相対パスの記法に似ています。

マーカー意味
(.)同じセグメントレベルをインターセプトfeed/(.)photo/[id]feed/photo/[id] を横取り
(..)1 つ上のセグメントをインターセプトfeed/(..)photo/[id]photo/[id] を横取り
(..)(..)2 つ上のセグメントをインターセプトめったに使わない
(...)app ルートから絶対パスでインターセプトfeed/(...)photo/[id] は app 直下の photo/[id] を横取り

マーカーは セグメント単位であり、ファイルシステム上のディレクトリ階層ではない点に注意してください。例えば app/(marketing)/blog/ のように Route Group (name) を挟んでいても、Route Group はセグメントとしてカウントされません。これは Next.js 公式の Intercepting Routes ドキュメント に明記されている挙動で、初学者がもっとも混乱するポイントです(私も最初は (..) の数え間違いで小一時間悩みました)。

重要な制約として、Intercepting Routes は クライアント側のソフトナビゲーション(<Link> 経由)でのみ発動します。URL を直接入力したりブラウザをリロードした場合は元のページがフルレンダリングされます。この性質こそが、後述するモーダルパターンを成立させています。

Parallel Routes と Intercepting Routes を組み合わせると、Instagram や Pinterest のような「サムネイルをクリックするとモーダルが開き、URL は /photo/123 に変わるが、リロードするとフルページが表示される」UX が実装できます。これは共有可能な深いリンクとモーダル UX を両立する、ソーシャル系アプリの定番パターンです。

ファイル構成は以下のようになります。

app/
├── layout.tsx              # modal スロットを受け取る
├── default.tsx             # ルート用 default
├── page.tsx                # ホーム(サムネ一覧)
├── photo/
│   └── [id]/
│       └── page.tsx        # フルページ版(直アクセス時)
└── @modal/
    ├── default.tsx         # null を返してモーダルなし状態
    └── (.)photo/
        └── [id]/
            └── page.tsx    # モーダル版(ソフトナビ時)

ルートレイアウトでスロットを受け取り、モーダルを children の上に重ねます。

// app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body>
        {children}
        {modal}
      </body>
    </html>
  );
}

// app/@modal/default.tsx
export default function Default() {
  return null;
}

// app/@modal/(.)photo/[id]/page.tsx
import { Modal } from "@/components/modal";
import { getPhoto } from "@/lib/photos";

export default async function PhotoModal({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const photo = await getPhoto(id);
  return (
    <Modal>
      <img src={photo.url} alt={photo.alt} />
      <p>{photo.caption}</p>
    </Modal>
  );
}

Modal コンポーネントは useRouter().back() で閉じるよう実装します。これにより、サムネをクリック → モーダル表示 → 閉じる、というフローが履歴上は単純な「進む・戻る」として記録され、ブラウザバックで自然に閉じられます。本番運用例は Vercel 公式の with-parallel-routes サンプル でも確認できます。認証が絡む場合は Next.js Middleware 完全ガイド で扱った Middleware パターンと組み合わせるのが定石です。

default.tsx を忘れるとどうなる?

Parallel Routes で最も頻発する不具合が、ソフトナビゲーション後にスロットが 404 になる問題です。原因はほぼ default.tsx の欠落で、私が前のプロジェクトで踏み抜いたのもまさにこれでした。Next.js のスロット解決ロジックを理解すれば防げます。

初回ロード(ハードナビゲーション)では、Next.js は URL からスロットがマッチするかを判定し、マッチしないスロットには default.tsx をレンダリングします。一方、ソフトナビゲーションでは Next.js は 「アクティブだったスロット状態」をクライアント側で保持します。しかしブラウザがリロードされると、サーバーは現在の URL からスロット状態を再現できないため、default.tsx がないスロットは 404 を返します。

// 必須:各スロットに default.tsx を置く
// app/@modal/default.tsx
export default function Default() {
  return null;  // モーダルを表示しないなら null でよい
}

// app/dashboard/@analytics/default.tsx
export default function Default() {
  // 直アクセス時に表示するフォールバック
  return <p>集計データを準備しています</p>;
}

条件付きレンダリングと認証ゲート

Parallel Routes は「ログインユーザーには @dashboard、未ログインには @login を見せる」のような条件分岐 UI にも使えます。レイアウト内でセッションを判定し、レンダリングするスロットを切り替えるだけです。

// app/layout.tsx
import { auth } from "@/auth";

export default async function RootLayout({
  children,
  dashboard,
  login,
}: {
  children: React.ReactNode;
  dashboard: React.ReactNode;
  login: React.ReactNode;
}) {
  const session = await auth();
  return (
    <html lang="ja">
      <body>
        {session ? dashboard : login}
        {children}
      </body>
    </html>
  );
}

この方式の利点は、両方のスロットが並列にプリレンダリングされるため、認証状態が確定した瞬間に即座に切り替わる点です。CLS(Cumulative Layout Shift)を抑えつつ、サーバーで認証を完結できます。サーバー側でのデータ取得とミューテーションのセキュリティ要件については Server Actions セキュリティ完全ガイド も併せて確認してください。

よくある落とし穴とデバッグ

実装中に遭遇しやすい問題と解決策を列挙します。

1. Intercepting Routes が発動しない

原因の 9 割は「<a> タグを使っている」「セグメントレベルの数え間違い」のどちらかです。必ず next/link<Link> を使い、マーカーの数は Route Group (name) を除外したセグメントで数えてください。

2. モーダルを開いた状態でリロードすると消える

これは Intercepting Routes の 仕様通りの挙動です。リロード時はインターセプトされる側(photo/[id]/page.tsx)が直接レンダリングされます。フルページ版を用意していない場合は 404 になるため、必ずインターセプトされる元のページも作成してください。

3. Turbopack でビルドが遅い/失敗する

Next.js 16 では Turbopack の本番ビルドが安定版になり、Intercepting Routes のコンパイル時間が大幅に短縮されました。古い next.config.jsexperimental.turbo を使っている場合は、turbopack キーに移行してください(詳細は Next.js 公式ブログのリリース情報 参照)。

4. スロットが key 衝突で再マウントを繰り返す

同じレイアウトに同名のセグメントを持つスロットを複数定義すると、React のリコンサイラがマウント/アンマウントを繰り返すことがあります。スロット名は必ずユニークにし、共通 UI はサーバーコンポーネントとして外側のレイアウトに切り出してください。

よくある質問

Parallel Routes と Intercepting Routes はどちらか片方だけでも使えますか?

はい、両者は独立した機能です。Parallel Routes はダッシュボードや認証ゲートで単体利用でき、Intercepting Routes も Parallel Routes なしでルート差し替えに使えます。組み合わせるのは主にモーダルパターンのときだけです。

Intercepting Routes はサーバーサイドレンダリングと両立しますか?

両立します。インターセプト版のページも Server Components として動作し、データフェッチや認証チェックをサーバーで完結できます。クライアント側で必要なのはモーダル UI の開閉ロジックだけです。

default.tsx と not-found.tsx の違いは何ですか?

default.tsx はスロットがマッチしないときに描画されるフォールバック、not-found.tsx はリソースが存在しないときの 404 ページです。Parallel Routes では両方を併用するのが安全で、default.tsx がないと意図せず 404 が出ます。

モーダルの URL をシェアされたらどう表示されますか?

シェアされた URL を直接開くと、Intercepting Routes は発動せずにフルページ版(app/photo/[id]/page.tsx)が表示されます。これは「モーダルではモバイル体験が悪い直リンク経由のユーザーにはフルページを返す」という意図的な挙動で、共有可能なディープリンクを実現します。

Parallel Routes は SEO に影響しますか?

影響は限定的です。各スロットは同じ URL の中でサーバーレンダリングされ、Googlebot にも単一の HTML として配信されます。ただし重要コンテンツを Suspense の遅いスロットに置くと、初回レンダリング時の HTML に含まれない可能性があるため、主要情報は children に置くのが安全です。

著者について Hannah Lindqvist

Hannah is a performance-focused frontend engineer with nine years building React applications, the last five exclusively on Next.js. She spent three years at Spotify on the Web Player team optimizing TTI on low-end devices in emerging markets, then two years at a Stockholm-based e-commerce platform where she rebuilt the storefront on App Router and edge runtime, hitting sub-200ms TTFB across European POPs. She currently freelances for DTC brands and SaaS companies that need a Core Web Vitals rescue, and occasionally contributes patches to next/font and the Vercel Speed Insights SDK. She has presented at React Advanced London and React Summit Amsterdam on streaming rendering patterns. Her tutorials are the ones with Lighthouse screenshots and flame graphs, because she doesn't trust benchmarks she can't reproduce. Strong preference for Playwright over Cypress and Vitest over Jest.