Параллельные маршруты (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 — это слот. Слоты объединяются с обычным компонентом Page для формирования окончательной страницы, связанной с сегментом маршрута. Из-за этого нельзя иметь отдельные статические и динамические слоты на одном уровне сегмента маршрута. Если один слот динамический, все слоты на этом уровне должны быть динамическими.

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

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

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 не может восстановить активное состояние родительской страницы.

Поведение

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

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

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

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

Примеры

С 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">Просмотры страниц</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>
    </>
  )
}

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

Параллельные маршруты можно использовать вместе с перехватывающими маршрутами (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>
  )
}

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

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

Теперь вы можете использовать маршрутизатор 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>
    </>
  )
}

При использовании компонента Link для перехода со страницы, которая не должна больше рендерить слот @auth, нам нужно убедиться, что параллельный маршрут соответствует компоненту, который возвращает 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 и т. д.) вы можете использовать слот catch-all:

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

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

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

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

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

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

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