T-CREATOR

Next.js の Parallel Routes & Intercepting Routes を図解で理解する最新入門

Next.js の Parallel Routes & Intercepting Routes を図解で理解する最新入門

Next.js の App Router に導入された Parallel Routes と Intercepting Routes は、現代の Web アプリケーション開発において革新的な機能です。これらの機能を理解することで、より複雑で高度なユーザーインターフェースを効率的に実装できるようになります。

本記事では、これらの新機能を図解を交えながら、初心者の方にもわかりやすく解説いたします。実際のコード例も豊富に用意しており、理論と実践の両面から理解を深めていただけます。

背景

App Router の登場とルーティングの進化

Next.js 13 で導入された App Router は、従来の Pages Router から大幅に進化したルーティングシステムです。この新しいシステムは、React Server Components やストリーミングなどの最新技術を活用し、より高速で効率的な Web アプリケーションの構築を可能にしています。

App Router では、ファイルシステムベースのルーティングが継続されながらも、新しい規約と機能が追加されました。特に注目すべきは、Parallel Routes と Intercepting Routes という 2 つの強力な機能です。

Next.js のルーティング進化を図で確認してみましょう。

mermaidflowchart TD
    pages[Pages Router] -->|進化| app[App Router]
    app --> server[Server Components]
    app --> streaming[ストリーミング]
    app --> parallel[Parallel Routes]
    app --> intercepting[Intercepting Routes]

    parallel --> ui1[複数UI同時表示]
    intercepting --> ui2[URL インターセプト]

    ui1 --> benefit1[ダッシュボード強化]
    ui2 --> benefit2[モーダル制御向上]

この図が示すように、App Router は従来の機能を基盤としながら、新しい可能性を開拓しています。

従来のルーティングの課題と新機能の必要性

従来の Pages Router では、1 つの URL に対して 1 つのページコンポーネントが対応する単純な構造でした。この仕組みは理解しやすい反面、以下のような課題がありました。

課題項目詳細内容影響範囲
単一ページ制約1URL に 1 ページのみ対応UI 設計の制限
状態管理複雑化モーダルやサイドバーの状態管理が困難開発効率低下
URL 構造制限柔軟な URL 設計が困難SEO 対応の限界

これらの課題を解決するため、Next.js チームは新しいルーティング機能の開発に着手しました。その結果生まれたのが、Parallel Routes と Intercepting Routes です。

課題

複雑な UI 構成での画面レンダリングの課題

現代の Web アプリケーションでは、単一画面に複数の独立したコンテンツエリアを表示することが一般的になっています。例えば、ダッシュボード画面では以下のような要素が同時に表示されます。

複雑な UI 構成の課題を図解で理解しましょう。

mermaidflowchart LR
    subgraph dashboard[ダッシュボード画面]
        nav[ナビゲーション]
        main[メインコンテンツ]
        sidebar[サイドバー]
        modal[モーダル]
    end

    subgraph problems[従来の課題]
        state[状態管理複雑化]
        render[レンダリング非効率]
        url[URL構造制限]
    end

    dashboard --> problems

    style problems fill:#ffcccc

従来のアプローチでは、これらの要素をすべて単一コンポーネント内で管理する必要があり、以下の問題が発生していました。

  • 状態管理の複雑化: 各エリアの表示/非表示状態を親コンポーネントで一元管理
  • レンダリングの非効率性: 一部エリアの更新で全体が再レンダリング
  • コード保守性の低下: 機能追加時の影響範囲が不明確

モーダル表示や条件分岐ルーティングの実装困難

特に困難だったのが、モーダル表示の実装です。従来の手法では、以下のような課題がありました。

URL とモーダル状態の不一致 モーダルを開いた状態でブラウザの更新ボタンを押すと、モーダルが閉じてしまう問題がありました。これは、モーダルの状態が URL に反映されていないためです。

直接 URL アクセスの対応困難 ユーザーがモーダル表示状態の URL を直接ブラウザに入力した場合、適切にモーダルを表示することが困難でした。

条件分岐ルーティングの課題を以下の図で確認しましょう。

mermaidstateDiagram-v2
    [*] --> リスト画面
    リスト画面 --> モーダル表示: アイテムクリック
    モーダル表示 --> リスト画面: モーダル閉じる

    state "従来の問題" as problems {
        URL反映なし
        直接アクセス不可
        状態管理複雑
    }

    モーダル表示 --> problems

これらの課題により、開発者は複雑な状態管理ライブラリを導入したり、独自のルーティング制御機能を実装したりする必要がありました。

解決策

Parallel Routes(並列ルート)の概念と仕組み

Parallel Routes は、同一画面内で複数の独立したルートを並列実行できる革新的な機能です。この機能により、各コンテンツエリアを独立したコンポーネントとして管理し、効率的なレンダリングが可能になります。

基本的な仕組み

Parallel Routes では、@folder という命名規則を使用してスロットを定義します。

typescript// app/dashboard/layout.tsx
export default function Layout({
  children,
  analytics, // @analytics スロット
  team, // @team スロット
  user, // @user スロット
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
  user: React.ReactNode;
}) {
  return (
    <div className='dashboard'>
      {children}
      <div className='analytics-section'>{analytics}</div>
      <div className='team-section'>{team}</div>
      <div className='user-section'>{user}</div>
    </div>
  );
}

このレイアウトコンポーネントは、複数のスロットを受け取り、それぞれを適切な位置に配置します。

ファイル構造とスロット定義

Parallel Routes を使用する際のディレクトリ構造は以下のようになります。

graphqlapp/
└── dashboard/
    ├── layout.tsx          # レイアウトコンポーネント
    ├── page.tsx           # メインコンテンツ
    ├── @analytics/        # アナリティクススロット
    │   └── page.tsx
    ├── @team/             # チームスロット
    │   └── page.tsx
    └── @user/             # ユーザースロット
        └── page.tsx

各スロットは独立してレンダリングされ、それぞれが独自のローディング状態やエラー処理を持つことができます。

Intercepting Routes(インターセプトルート)の概念と仕組み

Intercepting Routes は、特定のルートへのナビゲーションを「インターセプト(横取り)」して、別のコンポーネントを表示する機能です。これにより、モーダル表示や条件分岐ナビゲーションを elegant に実装できます。

インターセプト規則の理解

Intercepting Routes では、以下の規則を使用してインターセプト対象を指定します。

規則意味
(.)同じレベル(.)​/​photo​/​[id]
(..)1 つ上のレベル(..)​/​photo​/​[id]
(...)2 つ上のレベル(...)​/​photo​/​[id]
(....)+より上のレベル(....)​/​photo​/​[id]

モーダル実装でのインターセプト活用

画像ギャラリーでのモーダル表示を例に、Intercepting Routes の活用方法を見てみましょう。

typescript// app/gallery/@modal/(.)/photo/[id]/page.tsx
import { Modal } from '@/components/modal';
import { PhotoDetail } from '@/components/photo-detail';

export default function PhotoModal({
  params,
}: {
  params: { id: string };
}) {
  return (
    <Modal>
      <PhotoDetail id={params.id} />
    </Modal>
  );
}

このコンポーネントは、​/​photo​/​[id] へのナビゲーションをインターセプトし、モーダル形式で写真詳細を表示します。

Intercepting Routes の動作フローを図で確認しましょう。

mermaidsequenceDiagram
    participant User as ユーザー
    participant Gallery as ギャラリー画面
    participant Router as Next.js Router
    participant Modal as モーダル
    participant PhotoPage as 写真詳細ページ

    User->>Gallery: 写真をクリック
    Gallery->>Router: /photo/123 へナビゲート

    alt インターセプト発生
        Router->>Modal: モーダル表示
        Modal->>User: 写真をモーダルで表示
    else 直接アクセス
        Router->>PhotoPage: 通常ページ表示
        PhotoPage->>User: 写真を専用ページで表示
    end

この図が示すように、同じ URL でも アクセス方法によって異なるコンポーネントが表示されます。

2 つの機能の連携メカニズム

Parallel Routes と Intercepting Routes を組み合わせることで、より高度な UI パターンを実現できます。

統合アーキテクチャの設計

両機能を組み合わせた場合のアーキテクチャを図解します。

mermaidflowchart TD
    subgraph app[アプリケーション]
        subgraph layout[レイアウト]
            main[メインコンテンツ]
            sidebar[サイドバー]
            modal[モーダルスロット]
        end

        subgraph routes[ルーティング]
            parallel[Parallel Routes]
            intercepting[Intercepting Routes]
        end
    end

    parallel --> sidebar
    parallel --> main
    intercepting --> modal

    style parallel fill:#e1f5fe
    style intercepting fill:#f3e5f5

実装パターンの組み合わせ

以下は、ダッシュボードアプリケーションでの統合実装例です。

typescript// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  sidebar, // @sidebar Parallel Route
  modal, // @modal Intercepting Route
}: {
  children: React.ReactNode;
  sidebar: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <div className='dashboard-layout'>
      <aside className='sidebar'>{sidebar}</aside>
      <main className='main-content'>
        {children}
        {modal} {/* モーダルをオーバーレイ表示 */}
      </main>
    </div>
  );
}

この実装により、サイドバーは並列レンダリング、モーダルはインターセプトによる条件表示が実現されます。

具体例

Parallel Routes の実装例

実際のプロジェクトで Parallel Routes を活用したダッシュボード画面を構築してみましょう。

プロジェクト初期化とファイル構成

まず、必要なディレクトリ構造を作成します。

goapp/
└── dashboard/
    ├── layout.tsx
    ├── page.tsx
    ├── @analytics/
    │   ├── page.tsx
    │   ├── loading.tsx
    │   └── error.tsx
    ├── @notifications/
    │   ├── page.tsx
    │   ├── loading.tsx
    │   └── error.tsx
    └── @activity/
        ├── page.tsx
        ├── loading.tsx
        └── error.tsx

メインレイアウトの実装

ダッシュボードのレイアウトコンポーネントを実装します。

typescript// app/dashboard/layout.tsx
interface DashboardLayoutProps {
  children: React.ReactNode;
  analytics: React.ReactNode;
  notifications: React.ReactNode;
  activity: React.ReactNode;
}

export default function DashboardLayout({
  children,
  analytics,
  notifications,
  activity,
}: DashboardLayoutProps) {
  return (
    <div className='grid grid-cols-12 gap-6 p-6'>
      {/* メインコンテンツエリア */}
      <div className='col-span-8'>{children}</div>

      {/* サイドバーエリア */}
      <div className='col-span-4 space-y-6'>
        {/* アナリティクス */}
        <div className='bg-white rounded-lg shadow p-4'>
          <h3 className='text-lg font-semibold mb-4'>
            アナリティクス
          </h3>
          {analytics}
        </div>

        {/* 通知 */}
        <div className='bg-white rounded-lg shadow p-4'>
          <h3 className='text-lg font-semibold mb-4'>
            通知
          </h3>
          {notifications}
        </div>

        {/* アクティビティ */}
        <div className='bg-white rounded-lg shadow p-4'>
          <h3 className='text-lg font-semibold mb-4'>
            最近のアクティビティ
          </h3>
          {activity}
        </div>
      </div>
    </div>
  );
}

各スロットの実装

アナリティクススロットを実装します。

typescript// app/dashboard/@analytics/page.tsx
export default function AnalyticsSlot() {
  return (
    <div className='space-y-4'>
      <div className='grid grid-cols-2 gap-4'>
        <div className='text-center'>
          <div className='text-2xl font-bold text-blue-600'>
            1,234
          </div>
          <div className='text-sm text-gray-600'>
            総売上
          </div>
        </div>
        <div className='text-center'>
          <div className='text-2xl font-bold text-green-600'>
            567
          </div>
          <div className='text-sm text-gray-600'>
            新規顧客
          </div>
        </div>
      </div>
      <div className='h-32 bg-gray-100 rounded flex items-center justify-center'>
        <span className='text-gray-500'>グラフエリア</span>
      </div>
    </div>
  );
}

通知スロットの実装です。

typescript// app/dashboard/@notifications/page.tsx
export default function NotificationsSlot() {
  const notifications = [
    {
      id: 1,
      message: '新しい注文が届きました',
      time: '5分前',
    },
    {
      id: 2,
      message: '在庫が少なくなっています',
      time: '1時間前',
    },
    {
      id: 3,
      message: '月次レポートが生成されました',
      time: '2時間前',
    },
  ];

  return (
    <div className='space-y-3'>
      {notifications.map((notification) => (
        <div
          key={notification.id}
          className='border-l-4 border-blue-500 pl-3'
        >
          <p className='text-sm font-medium'>
            {notification.message}
          </p>
          <p className='text-xs text-gray-500'>
            {notification.time}
          </p>
        </div>
      ))}
    </div>
  );
}

Intercepting Routes の実装例

次に、画像ギャラリーでモーダル表示を実現する Intercepting Routes を実装します。

ギャラリーの基本構造

bashapp/
├── gallery/
│   ├── layout.tsx
│   ├── page.tsx
│   └── @modal/
│       └── (.)/photo/
│           └── [id]/
│               └── page.tsx
└── photo/
    └── [id]/
        └── page.tsx

ギャラリーレイアウトの実装

typescript// app/gallery/layout.tsx
export default function GalleryLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <div className='relative'>
      {children}
      {modal}
    </div>
  );
}

ギャラリー一覧ページの実装

typescript// app/gallery/page.tsx
import Link from 'next/link';
import Image from 'next/image';

const photos = [
  { id: '1', src: '/photos/1.jpg', alt: '写真1' },
  { id: '2', src: '/photos/2.jpg', alt: '写真2' },
  { id: '3', src: '/photos/3.jpg', alt: '写真3' },
];

export default function GalleryPage() {
  return (
    <div className='container mx-auto px-4 py-8'>
      <h1 className='text-3xl font-bold mb-8'>
        フォトギャラリー
      </h1>
      <div className='grid grid-cols-3 gap-4'>
        {photos.map((photo) => (
          <Link
            key={photo.id}
            href={`/photo/${photo.id}`}
            className='block relative aspect-square overflow-hidden rounded-lg hover:opacity-75 transition-opacity'
          >
            <Image
              src={photo.src}
              alt={photo.alt}
              fill
              className='object-cover'
            />
          </Link>
        ))}
      </div>
    </div>
  );
}

モーダル用インターセプトルートの実装

typescript// app/gallery/@modal/(.)/photo/[id]/page.tsx
'use client';

import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { useEffect } from 'react';

export default function PhotoModal({
  params,
}: {
  params: { id: string };
}) {
  const router = useRouter();

  // ESCキーでモーダルを閉じる
  useEffect(() => {
    const handleEsc = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        router.back();
      }
    };

    document.addEventListener('keydown', handleEsc);
    return () =>
      document.removeEventListener('keydown', handleEsc);
  }, [router]);

  return (
    <div className='fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50'>
      <div className='relative max-w-4xl max-h-full p-4'>
        {/* 閉じるボタン */}
        <button
          onClick={() => router.back()}
          className='absolute top-2 right-2 text-white text-2xl z-10 bg-black bg-opacity-50 rounded-full w-8 h-8 flex items-center justify-center'
        >
          ×
        </button>

        {/* 写真表示エリア */}
        <div className='relative'>
          <Image
            src={`/photos/${params.id}.jpg`}
            alt={`写真 ${params.id}`}
            width={800}
            height={600}
            className='rounded-lg'
          />
        </div>
      </div>
    </div>
  );
}

通常の写真詳細ページ

直接 URL アクセス時には、通常のページが表示されます。

typescript// app/photo/[id]/page.tsx
import Image from 'next/image';
import Link from 'next/link';

export default function PhotoPage({
  params,
}: {
  params: { id: string };
}) {
  return (
    <div className='container mx-auto px-4 py-8'>
      <Link
        href='/gallery'
        className='inline-block mb-6 text-blue-600 hover:underline'
      >
        ← ギャラリーに戻る
      </Link>

      <div className='max-w-4xl mx-auto'>
        <Image
          src={`/photos/${params.id}.jpg`}
          alt={`写真 ${params.id}`}
          width={800}
          height={600}
          className='w-full rounded-lg shadow-lg'
        />

        <div className='mt-6 p-6 bg-white rounded-lg shadow'>
          <h1 className='text-2xl font-bold mb-4'>
            写真 {params.id}
          </h1>
          <p className='text-gray-600'>
            この写真の詳細情報がここに表示されます。 直接URL
            でアクセスした場合は、この専用ページが表示されます。
          </p>
        </div>
      </div>
    </div>
  );
}

統合実装:画像ギャラリーとモーダル表示

最後に、Parallel Routes と Intercepting Routes を組み合わせた統合実装例を紹介します。

高度なダッシュボード実装

以下の実装では、ダッシュボードにフォトギャラリー機能を統合し、モーダル表示にも対応しています。

typescript// app/dashboard-advanced/layout.tsx
export default function AdvancedDashboardLayout({
  children,
  sidebar,
  modal,
  gallery,
}: {
  children: React.ReactNode;
  sidebar: React.ReactNode;
  modal: React.ReactNode;
  gallery: React.ReactNode;
}) {
  return (
    <div className='flex h-screen bg-gray-100'>
      {/* サイドバー */}
      <aside className='w-64 bg-white shadow-md'>
        {sidebar}
      </aside>

      {/* メインコンテンツエリア */}
      <main className='flex-1 overflow-auto'>
        <div className='p-6'>{children}</div>

        {/* ギャラリーセクション */}
        <div className='p-6 border-t'>{gallery}</div>
      </main>

      {/* モーダルオーバーレイ */}
      {modal}
    </div>
  );
}

Parallel Routes と Intercepting Routes の連携パターンを図で確認しましょう。

mermaidflowchart TD
    subgraph browser[ブラウザ]
        url[URL: /dashboard-advanced]
    end

    subgraph nextjs[Next.js App Router]
        router[ルーター]
        parallel[Parallel Routes処理]
        intercept[Intercepting Routes処理]
    end

    subgraph components[コンポーネント]
        layout[レイアウト]
        sidebar[サイドバー]
        main[メインコンテンツ]
        gallery[ギャラリー]
        modal[モーダル]
    end

    url --> router
    router --> parallel
    router --> intercept

    parallel --> sidebar
    parallel --> main
    parallel --> gallery

    intercept --> modal

    layout --> components

この統合実装により、以下の機能が実現されます。

図で理解できる要点

  • 複数のコンテンツエリアが独立してレンダリング
  • モーダル表示が URL 状態と同期
  • 直接アクセスとナビゲーションアクセスの使い分け

まとめ

新機能活用のメリットと今後の展望

Next.js の Parallel Routes と Intercepting Routes は、現代の Web アプリケーション開発における重要な課題を解決する革新的な機能です。

主要なメリット

メリット詳細開発への影響
独立レンダリング各コンテンツエリアが独立して更新パフォーマンス向上
URL 状態管理モーダルやサイドバーの状態が URL に反映SEO 対応・UX 向上
コード保守性機能ごとの関心の分離開発効率向上
直感的な実装ファイルシステムベースの設計学習コストの削減

活用シーンの広がり

これらの機能は、以下のようなシーンで特に威力を発揮します。

  • ダッシュボード系アプリケーション: 複数のデータ表示エリアの独立管理
  • EC サイト: 商品一覧とモーダル詳細表示の連携
  • SNS・メディアサイト: フィード表示とコンテンツ詳細のシームレス連携
  • 管理画面: サイドバー・メインコンテンツ・モーダルの統合管理

今後の展望

Next.js チームは、これらの機能をさらに発展させる計画を発表しています。特に注目すべきは以下の点です。

パフォーマンスの最適化 Parallel Routes での並列データフェッチングのさらなる最適化が予定されています。

開発者体験の向上 TypeScript サポートの強化やデバッグツールの改善が進められています。

エコシステムとの連携 Storybook や Testing Library などの開発ツールとの連携強化も計画されています。

Next.js の新機能は、Web アプリケーション開発の未来を大きく変える可能性を秘めています。早期に習得することで、より高品質で保守性の高いアプリケーションを効率的に構築できるようになるでしょう。

これらの機能を活用して、ユーザーにとってより快適で、開発者にとってより扱いやすい Web アプリケーションを構築していきましょう。

関連リンク