平行路由 (Parallel Routes)

平行路由 (Parallel Routes) 讓您可以在同一個佈局中同時或條件性地渲染一個或多個頁面。這對於應用程式中高度動態的部分非常有用,例如儀表板和社交網站的動態訊息流。

舉例來說,考慮一個儀表板,您可以使用平行路由同時渲染 teamanalytics 頁面:

平行路由圖示

約定

插槽 (Slots)

平行路由是透過命名的插槽 (slots) 來建立的。插槽使用 @folder 約定來定義。例如,以下檔案結構定義了兩個插槽:@analytics@team

平行路由檔案系統結構

插槽會作為屬性 (props) 傳遞給共享的父佈局。對於上面的例子,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}
    </>
  )
}

然而,插槽不是路由段 (route segments),也不會影響 URL 結構。例如,對於 /@analytics/views,URL 會是 /views,因為 @analytics 是一個插槽。插槽會與常規的頁面 (Page) 元件結合,形成與路由段相關的最終頁面。因此,您無法在同一路由段層級同時擁有獨立的靜態 (static)動態 (dynamic) 插槽。如果一個插槽是動態的,該層級的所有插槽都必須是動態的。

須知

  • children 屬性是一個隱含的插槽,不需要映射到資料夾。這意味著 app/page.js 等同於 app/@children/page.js

default.js

您可以定義一個 default.js 檔案,在初始載入或整頁重新載入時,為未匹配的插槽提供回退渲染。

考慮以下資料夾結構。@team 插槽有一個 /settings 頁面,但 @analytics 沒有。

平行路由未匹配的路由

當導航到 /settings 時,@team 插槽會渲染 /settings 頁面,同時保持 @analytics 插槽當前活動的頁面。

在重新整理時,Next.js 會為 @analytics 渲染 default.js。如果 default.js 不存在,則會渲染 404

此外,由於 children 是一個隱含的插槽,您也需要建立一個 default.js 檔案,以便在 Next.js 無法恢復父頁面的活動狀態時,為 children 提供回退渲染。

行為

預設情況下,Next.js 會追蹤每個插槽的活動狀態(或子頁面)。然而,插槽中渲染的內容將取決於導航類型:

  • 軟導航 (Soft Navigation):在客戶端導航期間,Next.js 會執行部分渲染 (partial render),改變插槽內的子頁面,同時保持其他插槽的活動子頁面,即使它們與當前 URL 不匹配。
  • 硬導航 (Hard Navigation):在整頁載入(瀏覽器重新整理)後,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
}

標籤群組 (Tab Groups)

您可以在插槽內添加一個 layout,允許用戶獨立導航該插槽。這對於建立標籤非常有用。

例如,@analytics 插槽有兩個子頁面:/page-views/visitors

帶有兩個子頁面和佈局的 analytics 插槽

@analytics 內,建立一個 layout 檔案,以在兩個頁面之間共享標籤:

import Link from 'next/link'

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Link href="/page-views">Page Views</Link>
        <Link href="/visitors">Visitors</Link>
      </nav>
      <div>{children}</div>
    </>
  )
}
import Link from 'next/link'

export default function Layout({ children }) {
  return (
    <>
      <nav>
        <Link href="/page-views">Page Views</Link>
        <Link href="/visitors">Visitors</Link>
      </nav>
      <div>{children}</div>
    </>
  )
}

模態框 (Modals)

平行路由可以與攔截路由 (Intercepting Routes) 一起使用,以建立支援深層連結的模態框。這讓您可以解決建立模態框時的常見挑戰,例如:

  • 讓模態框內容可透過 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 插槽內,添加一個 default.js 檔案,返回 null。這確保了模態框在不活動時不會被渲染。

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>
  )
}

須知

  • 用於攔截路由的約定(例如 (.))取決於您的檔案系統結構。請參閱攔截路由約定
  • 透過將 <Modal> 功能與模態框內容(<Login>)分開,您可以確保模態框內的任何內容(例如表單)都是伺服器元件。有關更多資訊,請參閱交錯客戶端和伺服器元件

打開模態框

現在,您可以利用 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">Open modal</Link>
      </nav>
      <div>{auth}</div>
      <div>{children}</div>
    </>
  )
}
import Link from 'next/link'

export default function Layout({ auth, children }) {
  return (
    <>
      <nav>
        <Link href="/login">Open modal</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()
        }}
      >
        Close modal
      </button>
      <div>{children}</div>
    </>
  )
}
'use client'

import { useRouter } from 'next/navigation'

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

  return (
    <>
      <button
        onClick={() => {
          router.back()
        }}
      >
        Close modal
      </button>
      <div>{children}</div>
    </>
  )
}

當使用 Link 元件導航到不應該再渲染 @auth 插槽的頁面時,我們需要確保平行路由匹配到一個返回 null 的元件。例如,當導航回根頁面時,我們建立一個 @auth/page.tsx 元件:

import Link from 'next/link'

export function Modal({ children }: { children: React.ReactNode }) {
  return (
    <>
      <Link href="/">Close modal</Link>
      <div>{children}</div>
    </>
  )
}
import Link from 'next/link'

export function Modal({ children }) {
  return (
    <>
      <Link href="/">Close modal</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錯誤處理文件。