1. ホーム
  2. Next.js

【Next.js】Parallel Routes(@folder)の使い方|1つの画面に複数のページを同時に表示する

Share

Next.js の App Router には、1つの画面の中に複数のページを同時に表示するための Parallel Routes(並行ルート)という仕組みがあります。たとえばダッシュボードで、左側にチーム一覧、右側に分析グラフを別々のページとして並べたいとき、それぞれを独立したファイルとして管理しながら1つのレイアウトにまとめられます。定義に使うのは @folder という名前のフォルダで、これはスロット(差し込み口)を表します。この記事では、Parallel Routes の基本的な考え方から、スロットが layout.tsx にどう渡されるか、ダッシュボードでの実践例、フォールバック用の default.tsx、スロットごとの読み込み・エラー表示、そしてつまずきやすいポイントまでを、具体的なフォルダ構成とあわせて解説します。

Parallel Routes で何ができるのか

通常の App Router では、1つのレイアウトの中に表示されるページは children として渡される1つだけです。ところが実際の画面では、1つのレイアウトの中に独立した領域を複数持ちたいことがよくあります。管理画面のダッシュボードで、チームメンバーの一覧と、アクセス分析のグラフを左右に並べるような構成です。

Parallel Routes は、こうした複数の領域をそれぞれ独立したページ(スロット)として定義し、同じレイアウトの中に同時に表示するための仕組みです。各スロットは別々のフォルダに分かれているため、読み込み中の表示やエラー処理も領域ごとに独立して管理できます。片方だけを再読み込みしたり、ログイン状態によって表示するスロットを切り替えたり、といった柔軟な構成が可能になります。

スロットは @folder という名前のフォルダで定義する

スロットは、@ から始まる名前のフォルダで定義します。たとえば @team@analytics のようにします。この @ 付きのフォルダはあくまでスロットの名前を表すためのもので、URL には一切影響しません。Route Groups の (folder) と同じく、フォルダ構成を整理しつつパスには現れない特殊なフォルダだと考えると分かりやすいです。

まずは基本となるフォルダ構成を見てみます。dashboard フォルダの下に、通常の page.tsx に加えて @team@analytics という2つのスロットを置いています。

ディレクトリ構成
app/
└─ dashboard/
   ├─ layout.tsx        ← スロットを受け取って配置する
   ├─ page.tsx          → /dashboard(children に対応)
   ├─ @team/
   │  └─ page.tsx       ← team スロットの中身
   └─ @analytics/
      └─ page.tsx       ← analytics スロットの中身

この構成でも、生成される URL は /dashboard のままです。@team@analytics という文字列は URL に現れません。@team/page.tsx@analytics/page.tsx は、それぞれ独立したページとして、親の layout.tsx の中に同時に差し込まれます。

スロットは layout.tsx に名前付き props として渡される

Parallel Routes を理解するうえで一番大切なのが、スロットが親の layout.tsx にどう渡されるかです。通常のレイアウトは children という props だけを受け取りますが、スロットを定義すると、children に加えてスロット名と同じ名前の props が増えます。@team フォルダを作れば team@analytics フォルダを作れば analytics という props が、それぞれのスロットの中身として渡されます。

先ほどのダッシュボードのレイアウトを、実際に書いてみます。引数の分割代入で children と一緒に teamanalytics を受け取り、それらを好きな位置に配置します。

app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  team,
  analytics,
}: {
  children: React.ReactNode;
  team: React.ReactNode;      // @team スロットの中身
  analytics: React.ReactNode; // @analytics スロットの中身
}) {
  return (
    <div>
      {/* page.tsx の内容がここに入る */}
      {children}

      {/* 2つのスロットを左右に並べる */}
      <div style={{ display: "flex", gap: "16px" }}>
        <section style={{ flex: 1 }}>{team}</section>
        <section style={{ flex: 1 }}>{analytics}</section>
      </div>
    </div>
  );
}

このように、@team@analytics のフォルダ名が、そのまま teamanalytics という props 名に対応します。children はいつも通り、同じ階層の page.tsx の内容が渡されます。スロットとフォルダ名、props 名の対応を整理すると次のとおりです。

フォルダ受け取る props 名中身
page.tsxchildren同じ階層のページ
@team/teamteam スロットのページ
@analytics/analyticsanalytics スロットのページ

ダッシュボードを左右2画面で構成する

ここまでの内容を踏まえて、実際に動くスロットの中身も用意してみます。各スロットの page.tsx は、通常のページと同じく普通のコンポーネントを返すだけです。まずはチーム一覧のスロットです。

app/dashboard/@team/page.tsx
export default function TeamSlot() {
  return (
    <div>
      <h2>チームメンバー</h2>
      <ul>
        <li>田中さん</li>
        <li>佐藤さん</li>
      </ul>
    </div>
  );
}

続いて分析グラフのスロットです。こちらも独立したページなので、必要なデータの取得やコンポーネントの読み込みをこのファイルの中で完結できます。

app/dashboard/@analytics/page.tsx
export default function AnalyticsSlot() {
  return (
    <div>
      <h2>アクセス分析</h2>
      <p>今週の訪問数: 1,234</p>
    </div>
  );
}

これで /dashboard にアクセスすると、layout.tsx の中でチーム一覧と分析グラフが左右に並んで表示されます。それぞれが別々のファイルに分かれているため、担当ごとに独立して開発でき、片方の変更がもう片方に影響しにくいという保守上のメリットもあります。

マッチしないスロットは default.tsx で補う

Parallel Routes を使ううえで欠かせないのが default.tsx です。これは、あるスロットに対して表示すべき内容が見つからないときのフォールバック(代替表示)を定義するファイルです。スロットは URL の状態に応じて中身が切り替わりますが、現在の URL にマッチするページがそのスロットに存在しない場合があります。そのときに何を表示するかを default.tsx で指定します。

app/dashboard/@analytics/default.tsx
export default function AnalyticsDefault() {
  // このスロットにマッチするページがないときに表示される
  return null;
}

ここで注意したいのが、ソフトナビゲーション(アプリ内のリンククリックによる画面遷移)とフルリロード(ブラウザの再読み込み)で挙動が異なる点です。ソフトナビゲーションのときは、Next.js がスロットの直前の状態を覚えているため、マッチするページがなくても以前表示していた内容を保ったまま遷移できます。ところがフルリロードのときは、Next.js は各スロットの以前の状態を復元できません。そのため、現在の URL にマッチしないスロットについては default.tsx を探しにいきます。

このとき default.tsx が存在しないと、Next.js はそのスロットをどう描画してよいか分からず、ページ全体が 404 になってしまいます。つまり、スロットを1つでも定義したら、フルリロードに備えて各スロットに default.tsx を用意しておくのが基本になります。何も表示したくないスロットであっても、return null; を返す default.tsx を置いておくと安全です。

スロットごとに読み込み・エラー表示を分ける

Parallel Routes の便利な点は、スロットが独立したルートとして扱われるため、読み込み中やエラー時の表示もスロットごとに分けられることです。各スロットのフォルダに loading.tsx を置けば、そのスロットのデータ読み込み中だけローディング表示を出せます。同様に error.tsx を置けば、そのスロットで発生したエラーだけを局所的に受け止められます。

ディレクトリ構成
app/
└─ dashboard/
   ├─ layout.tsx
   ├─ @analytics/
   │  ├─ page.tsx
   │  ├─ loading.tsx    ← analytics だけの読み込み表示
   │  ├─ error.tsx      ← analytics だけのエラー表示
   │  └─ default.tsx
   └─ @team/
      ├─ page.tsx
      └─ default.tsx

こうしておくと、分析グラフの取得に時間がかかっている間も、チーム一覧はそのまま表示され、分析グラフの領域だけにローディングが出ます。片方のスロットでエラーが起きても、その領域だけがエラー表示に置き換わり、もう片方は問題なく表示され続けます。画面全体が読み込み中になったり、丸ごとエラー画面になったりしないので、ユーザー体験を保ちやすくなります。

また、スロットは layout.tsx の中で普通の props として扱えるため、条件付きレンダリングにも使えます。たとえばログイン状態に応じて、管理者向けのスロットとゲスト向けのスロットを出し分ける、といったことができます。

app/dashboard/layout.tsx(条件付きの例)
export default function DashboardLayout({
  team,
  analytics,
}: {
  team: React.ReactNode;
  analytics: React.ReactNode;
}) {
  const isLoggedIn = checkAuth(); // ログイン状態を判定する想定

  // ログインしていれば分析、していなければチーム紹介を表示
  return <div>{isLoggedIn ? analytics : team}</div>;
}

このように、スロットは単なる props なので、表示する・しないをレイアウト側のロジックで自由に制御できます。同じ URL のまま、状態に応じて画面の一部を丸ごと差し替えられるのが Parallel Routes の強みです。

リロードで 404 になるときに確認すること

Parallel Routes でつまずきやすいのが、アプリ内のリンク遷移では正常に動くのに、ブラウザで再読み込みすると突然 404 になるという症状です。これはほとんどの場合、スロットに default.tsx が用意されていないことが原因です。前述のとおり、フルリロード時にはスロットの以前の状態が復元できず、現在の URL にマッチしないスロットは default.tsx を探します。ここでフォールバックが見つからないと、ページ全体が 404 として扱われます。

対処は単純で、定義したすべてのスロットに default.tsx を置くことです。表示するものがないスロットは return null; を返しておけば十分です。ソフトナビゲーションでは動いてしまうため見落としがちですが、公開前には必ずブラウザの再読み込みでも動作するか確認しておきましょう。

@folder は URL セグメントにならない

もう1つ誤解しやすいのが、@folder が URL の一部になると思い込んでしまうケースです。@team というフォルダを作っても、/dashboard/team のような URL でアクセスできるようになるわけではありません。@ から始まるフォルダはあくまでスロットの名前を宣言するための命名規則であり、ルーティングの経路そのものには一切現れません。

この点で混同しやすいのが、動的ルートで使う [folder] や、整理用の (folder) との違いです。それぞれの記号が URL にどう影響するかを整理しておくと、取り違えを防げます。

書き方意味URL への影響
@folderParallel Routes(スロット)URL に含まれない
(folder)Route Group(整理用)URL に含まれない
[folder]Dynamic Route(動的ルート)URL の可変部分になる

@folder はスロットを親レイアウトに渡すための仕組みで、URL には現れません。特定のパスでスロットの中身を切り替えたいときは、スロットのフォルダの中にさらに通常のフォルダを作り、そのフォルダ名で経路を分けることになります。@ 自体はパスにならない、という原則を押さえておきましょう。

まとめ

Parallel Routes は、1つのレイアウトの中に複数のページ(スロット)を同時に表示するための App Router の仕組みです。スロットは @team@analytics のように @ から始まるフォルダで定義し、その中身は親の layout.tsxteamanalytics という名前付き props として渡されます。@ 付きのフォルダは URL には影響しないので、URL は変えずに画面を複数の領域に分割できます。スロットごとに loading.tsxerror.tsx を置いて読み込み・エラー表示を独立させたり、レイアウト側の条件分岐で表示を出し分けたりできるのも強みです。一方で、各スロットに default.tsx を用意しておかないと、ブラウザの再読み込み時に 404 になる点には注意が必要です。ソフトナビゲーションでは動いてもフルリロードで崩れることがあるため、公開前の確認を忘れないようにしましょう。

参考ページ