Параллельные маршруты (Parallel Routes)

Параллельные маршруты (Parallel Routes) позволяют одновременно или условно рендерить одну или несколько страниц в рамках одного макета. Они полезны для высокодинамичных разделов приложения, таких как дашборды и ленты в социальных сетях.

Например, для дашборда можно использовать параллельные маршруты, чтобы одновременно отображать страницы team и analytics:

Диаграмма параллельных маршрутов

Слоты

Параллельные маршруты создаются с использованием именованных слотов. Слоты определяются с помощью соглашения @folder. Например, следующая структура файлов определяет два слота: @analytics и @team:

Структура файловой системы для параллельных маршрутов

Слоты передаются как пропсы в общий родительский макет. Для примера выше компонент в 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 URL будет /views, так как @analytics — это слот.

Полезно знать:

  • Пропс children является неявным слотом, который не нужно сопоставлять с папкой. Это означает, что app/page.js эквивалентен app/@children/page.js.

Активное состояние и навигация

По умолчанию Next.js отслеживает активное состояние (или подстраницу) для каждого слота. Однако контент, рендерящийся внутри слота, зависит от типа навигации:

  • Мягкая навигация: При клиентской навигации Next.js выполнит частичный рендеринг, изменяя подстраницу внутри слота, сохраняя активные подстраницы других слотов, даже если они не соответствуют текущему URL.
  • Жесткая навигация: После полной загрузки страницы (обновления браузера) Next.js не может определить активное состояние для слотов, которые не соответствуют текущему URL. Вместо этого он отрендерит файл default.js для несопоставленных слотов или 404, если default.js не существует.

Полезно знать:

  • 404 для несопоставленных маршрутов помогает убедиться, что вы случайно не отрендерите параллельный маршрут на странице, для которой он не предназначен.

default.js

Вы можете определить файл default.js для рендеринга в качестве запасного варианта для несопоставленных слотов во время начальной загрузки или полного обновления страницы.

Рассмотрим следующую структуру папок. Слот @team имеет страницу /settings, но @analytics — нет.

Несопоставленные маршруты в параллельных маршрутах

При переходе на /settings слот @team отрендерит страницу /settings, сохраняя текущую активную страницу для слота @analytics.

При обновлении Next.js отрендерит default.js для @analytics. Если default.js не существует, вместо этого будет отрендерен 404.

Кроме того, поскольку children является неявным слотом, вам также нужно создать файл default.js для рендеринга запасного варианта для children, когда Next.js не может восстановить активное состояние родительской страницы.

useSelectedLayoutSegment(s)

И useSelectedLayoutSegment, и useSelectedLayoutSegments принимают параметр 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 (или /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.

Слот 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>
    </>
  )
}

Модальные окна

Параллельные маршруты можно использовать вместе с Перехватывающими маршрутами для создания модальных окон. Это позволяет решить распространенные проблемы при создании модальных окон, такие как:

  • Возможность делиться контентом модального окна через 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>
  )
}

Полезно знать:

Открытие модального окна

Теперь вы можете использовать маршрутизатор 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, мы используем catch-all маршрут, который возвращает null.

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

Полезно знать:

  • Мы используем catch-all маршрут в нашем слоте @auth для закрытия модального окна из-за поведения, описанного в Активное состояние и навигация. Поскольку клиентская навигация на маршрут, который больше не соответствует слоту, будет оставаться видимой, нам нужно сопоставить слот с маршрутом, который возвращает null, чтобы закрыть модальное окно.
  • Другие примеры могут включать открытие модального окна с фотографией в галерее, одновременно имея отдельную страницу /photo/[id], или открытие корзины покупок в боковом модальном окне.
  • Посмотрите пример модальных окон с перехватывающими и параллельными маршрутами.

Загрузка и обработка ошибок

Параллельные маршруты могут стримиться независимо, позволяя вам определять независимые состояния ошибок и загрузки для каждого маршрута:

Параллельные маршруты позволяют настраивать состояния ошибок и загрузки

См. документацию по UI загрузки и обработке ошибок для получения дополнительной информации.