Параллельные маршруты (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
создайте файл 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>
)
}
Полезно знать:
- Соглашение, используемое для перехвата маршрута, например
(.)
, зависит от структуры вашей файловой системы. См. Конвенцию перехватывающих маршрутов.- Разделяя функциональность
<Modal>
и контент модального окна (<Login>
), вы можете гарантировать, что любой контент внутри модального окна, например формы, будет Server Components. См. Чередование клиентских и серверных компонентов для получения дополнительной информации.
Открытие модального окна
Теперь вы можете использовать маршрутизатор 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 загрузки и обработке ошибок для получения дополнительной информации.