パラレルルート (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 になります。

知っておくと便利:

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

アクティブ状態とナビゲーション

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

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

知っておくと便利:

  • 一致しないルートに対する404は、意図しないページでパラレルルートがレンダリングされるのを防ぎます。

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ファイルを作成する必要があります。

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内に、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 }: { children: React.ReactNode }) {
  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を返すキャッチオールルートを使用します。

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 CatchAll() {
  return null
}
export default function CatchAll() {
  return null
}

知っておくと便利:

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

ローディングとエラーUI

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

パラレルルートによるカスタムエラーとローディング状態の実現

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