パラレルルート (Parallel Routes)

パラレルルート (Parallel Routes) を使用すると、同じレイアウト内で複数のページを同時または条件付きでレンダリングできます。ダッシュボードやソーシャルサイトのフィードなど、アプリケーションの高度に動的なセクションに有用です。

例えば、ダッシュボードでは teamanalytics ページを同時にレンダリングするためにパラレルルートを使用できます:

パラレルルートの図解

規約

スロット

パラレルルートは名前付きスロットを使用して作成されます。スロットは @folder 規約で定義されます。例えば、次のファイル構造は @analytics@team の2つのスロットを定義しています:

パラレルルートのファイルシステム構造

スロットは共有親レイアウトにプロップスとして渡されます。上記の例では、app/layout.js のコンポーネントが @analytics@team スロットのプロップスを受け取り、children プロップスと並行してレンダリングできます:

export default function Layout({
  children,
  team,
  analytics,
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <>
      {children}
      {team}
      {analytics}
    </>
  )
}
export default function Layout({ children, team, analytics }) {
  return (
    <>
      {children}
      {team}
      {analytics}
    </>
  )
}

ただし、スロットはルートセグメントではなく、URL構造に影響しません。例えば、/@analytics/views の場合、@analytics はスロットなのでURLは /views になります。スロットは通常のページコンポーネントと組み合わされ、ルートセグメントに関連付けられた最終的なページを形成します。このため、同じルートセグメントレベルで静的動的のスロットを同時に持つことはできません。1つのスロットが動的の場合、そのレベルのすべてのスロットは動的でなければなりません。

知っておくと良いこと:

  • children プロップスはフォルダにマップする必要のない暗黙的なスロットです。つまり app/page.jsapp/@children/page.js と同等です。

default.js

初期ロードやフルページリロード時にマッチしないスロットのフォールバックとしてレンダリングする default.js ファイルを定義できます。

次のフォルダ構造を考えてみましょう。@team スロットには /settings ページがありますが、@analytics にはありません。

パラレルルートのマッチしないルート

/settings にナビゲートすると、@team スロットは /settings ページをレンダリングし、@analytics スロットの現在アクティブなページを維持します。

リフレッシュ時、Next.js は @analytics に対して default.js をレンダリングします。default.js が存在しない場合、代わりに 404 がレンダリングされます。

さらに、children は暗黙的なスロットなので、Next.js が親ページのアクティブな状態を回復できない場合に children のフォールバックをレンダリングするためにも default.js ファイルを作成する必要があります。

動作

デフォルトでは、Next.js は各スロットのアクティブな_状態_(またはサブページ)を追跡します。ただし、スロット内でレンダリングされるコンテンツはナビゲーションのタイプによって異なります:

  • ソフトナビゲーション: クライアントサイドナビゲーション時、Next.js は部分レンダリングを実行し、スロット内のサブページを変更しながら、他のスロットのアクティブなサブページを維持します(現在のURLと一致しなくても)。
  • ハードナビゲーション: フルページロード(ブラウザリフレッシュ)後、Next.js は現在のURLと一致しないスロットのアクティブな状態を判断できません。代わりに、マッチしないスロットに対して default.js ファイルをレンダリングし、default.js が存在しない場合は 404 をレンダリングします。

知っておくと良いこと:

  • マッチしないルートに対する 404 は、意図しないページでパラレルルートを誤ってレンダリングしないようにするのに役立ちます。

useSelectedLayoutSegment(s) の使用

useSelectedLayoutSegmentuseSelectedLayoutSegments はどちらも parallelRoutesKey パラメータを受け取り、スロット内のアクティブルートセグメントを読み取ることができます。

'use client'

import { useSelectedLayoutSegment } from 'next/navigation'

export default function Layout({ auth }: { auth: React.ReactNode }) {
  const loginSegment = useSelectedLayoutSegment('auth')
  // ...
}
'use client'

import { useSelectedLayoutSegment } from 'next/navigation'

export default function Layout({ auth }) {
  const loginSegment = useSelectedLayoutSegment('auth')
  // ...
}

ユーザーが app/@auth/login(またはURLバーの /login)にナビゲートすると、loginSegment は文字列 "login" と等しくなります。

条件付きルート

パラレルルートを使用して、ユーザーロールなどの条件に基づいてルートを条件付きでレンダリングできます。例えば、/admin または /user ロールに対して異なるダッシュボードページをレンダリングする場合:

条件付きルートの図解
import { checkUserRole } from '@/lib/auth'

export default function Layout({
  user,
  admin,
}: {
  user: React.ReactNode
  admin: React.ReactNode
}) {
  const role = checkUserRole()
  return role === 'admin' ? admin : user
}
import { checkUserRole } from '@/lib/auth'

export default function Layout({ user, admin }) {
  const role = checkUserRole()
  return role === 'admin' ? admin : user
}

タブグループ

スロット内に layout を追加して、ユーザーがスロットを独立してナビゲートできるようにできます。これはタブを作成するのに便利です。

例えば、@analytics スロットには /page-views/visitors の2つのサブページがあります。

レイアウトを持つ2つのサブページを持つanalyticsスロット

@analytics 内に、2つのページ間でタブを共有するための layout ファイルを作成します:

import Link from 'next/link'

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Link href="/page-views">ページビュー</Link>
        <Link href="/visitors">訪問者</Link>
      </nav>
      <div>{children}</div>
    </>
  )
}
import Link from 'next/link'

export default function Layout({ children }) {
  return (
    <>
      <nav>
        <Link href="/page-views">ページビュー</Link>
        <Link href="/visitors">訪問者</Link>
      </nav>
      <div>{children}</div>
    </>
  )
}

モーダル

パラレルルートはインターセプトルートと組み合わせて、ディープリンクをサポートするモーダルを作成できます。これにより、モーダル構築時の一般的な課題を解決できます:

  • モーダルコンテンツをURLで共有可能にする
  • ページがリフレッシュされたときにコンテキストを保持し、モーダルを閉じない
  • 前のルートに戻るのではなく、戻るナビゲーションでモーダルを閉じる
  • 進むナビゲーションでモーダルを再開する

次のUIパターンを考えてみましょう。ユーザーはクライアントサイドナビゲーションを使用してレイアウトからログインモーダルを開くか、別の /login ページにアクセスできます:

パラレルルートの図解

このパターンを実装するには、まずメインのログインページをレンダリングする /login ルートを作成します。

パラレルルートのモーダルログインページ
import { Login } from '@/app/ui/login'

export default function Page() {
  return <Login />
}
import { Login } from '@/app/ui/login'

export default function Page() {
  return <Login />
}

次に、@auth スロット内に null を返す default.js ファイルを追加します。これにより、モーダルがアクティブでないときにレンダリングされなくなります。

export default function Default() {
  return null
}
export default function Default() {
  return null
}

@auth スロット内で、/(.)login フォルダを更新して /login ルートをインターセプトします。<Modal> コンポーネントとその子を /(.)login/page.tsx ファイルにインポートします:

import { Modal } from '@/app/ui/modal'
import { Login } from '@/app/ui/login'

export default function Page() {
  return (
    <Modal>
      <Login />
    </Modal>
  )
}
import { Modal } from '@/app/ui/modal'
import { Login } from '@/app/ui/login'

export default function Page() {
  return (
    <Modal>
      <Login />
    </Modal>
  )
}

知っておくと良いこと:

モーダルを開く

Next.js ルーターを活用してモーダルを開閉できます。これにより、モーダルが開いているときや前後にナビゲートするときにURLが正しく更新されます。

モーダルを開くには、@auth スロットを親レイアウトにプロップスとして渡し、children プロップスと一緒にレンダリングします。

import Link from 'next/link'

export default function Layout({
  auth,
  children,
}: {
  auth: React.ReactNode
  children: React.ReactNode
}) {
  return (
    <>
      <nav>
        <Link href="/login">モーダルを開く</Link>
      </nav>
      <div>{auth}</div>
      <div>{children}</div>
    </>
  )
}
import Link from 'next/link'

export default function Layout({ auth, children }) {
  return (
    <>
      <nav>
        <Link href="/login">モーダルを開く</Link>
      </nav>
      <div>{auth}</div>
      <div>{children}</div>
    </>
  )
}

ユーザーが <Link> をクリックすると、/login ページにナビゲートする代わりにモーダルが開きます。ただし、リフレッシュ時や初期ロード時、/login にナビゲートするとメインのログインページに移動します。

モーダルを閉じる

router.back() を呼び出すか、Link コンポーネントを使用してモーダルを閉じることができます。

'use client'

import { useRouter } from 'next/navigation'

export function Modal({ children }: { children: React.ReactNode }) {
  const router = useRouter()

  return (
    <>
      <button
        onClick={() => {
          router.back()
        }}
      >
        モーダルを閉じる
      </button>
      <div>{children}</div>
    </>
  )
}
'use client'

import { useRouter } from 'next/navigation'

export function Modal({ children }) {
  const router = useRouter()

  return (
    <>
      <button
        onClick={() => {
          router.back()
        }}
      >
        モーダルを閉じる
      </button>
      <div>{children}</div>
    </>
  )
}

@auth スロットをレンダリングすべきでないページから離れるために Link コンポーネントを使用する場合、null を返すコンポーネントにパラレルルートがマッチするようにする必要があります。例えば、ルートページに戻る場合、@auth/page.tsx コンポーネントを作成します:

import Link from 'next/link'

export function Modal({ children }: { children: React.ReactNode }) {
  return (
    <>
      <Link href="/">モーダルを閉じる</Link>
      <div>{children}</div>
    </>
  )
}
import Link from 'next/link'

export function Modal({ children }) {
  return (
    <>
      <Link href="/">モーダルを閉じる</Link>
      <div>{children}</div>
    </>
  )
}
export default function Page() {
  return null
}
export default function Page() {
  return null
}

または、他のページ(/foo/foo/bar など)にナビゲートする場合は、キャッチオールスロットを使用できます:

export default function CatchAll() {
  return null
}
export default function CatchAll() {
  return null
}

知っておくと良いこと:

  • モーダルを閉じるために @auth スロットでキャッチオールルートを使用しているのは、パラレルルートの動作(#behavior)によるものです。スロットにマッチしなくなったルートへのクライアントサイドナビゲーションは表示されたままになるため、モーダルを閉じるには null を返すルートにマッチさせる必要があります。
  • 他の例としては、ギャラリーで写真モーダルを開きながら専用の /photo/[id] ページを持つことや、サイドモーダルでショッピングカートを開くことが含まれます。
  • インターセプトとパラレルルートを使用したモーダルの例を参照してください。

ローディングとエラーUI

パラレルルートは独立してストリーミングできるため、各ルートに対して独立したエラーとローディング状態を定義できます:

パラレルルートはカスタムエラーとローディング状態を可能にする

詳細はローディングUIエラーハンドリングのドキュメントを参照してください。