Пошук уроків, статей та іншого контенту
Створюйте паралельні маршрути для незалежного рендерингу кількох областей інтерфейсу в межах одного layout.
Паралельні маршрути в Next.js App Router дають змогу одночасно рендерити кілька незалежних областей інтерфейсу в межах одного layout.
Це корисно для сторінок, де різні частини інтерфейсу мають власну навігацію та стан:
панель керування з кількома віджетами;
сторінка з основним вмістом і бічними панелями;
інтерфейс адміністратора;
поштовий клієнт із деревом папок і списком повідомлень;
сторінка з модальним вікном, яке має власний маршрут.
На відміну від звичайних вкладених маршрутів, паралельні маршрути передаються до одного layout як окремі props.
Паралельний маршрут створюється директорією зі спеціальним префіксом @.
Наприклад:
app/
└── dashboard/
├── layout.jsx
├── page.jsx
├── @revenue/
│ └── page.jsx
└── @activity/
└── page.jsxДиректорії @revenue і @activity називаються слотами.
Важливо:
ім’я @revenue не стає частиною URL;
ім’я слота стає назвою prop у батьківському layout;
слот children є неявним і відповідає звичайному page.jsx.
Для цієї структури URL буде таким:
/dashboardА не:
/dashboard/revenue
/dashboard/activityУсі три області можуть бути відрендерені одночасно за адресою /dashboard.
Файл layout.jsx отримує слот children та всі іменовані слоти як props:
export default function DashboardLayout({
children,
revenue,
activity,
}) {
return (
<main>
<section>{children}</section>
<aside>
{revenue}
{activity}
</aside>
</main>
);
}Назви props повинні збігатися з назвами директорій без символу @:
| Директорія | Prop у layout | |---|---| | @revenue | revenue | | @activity | activity | | children | children |
Слоти є React-вузлами, тому їх можна розміщувати в будь-яких частинах розмітки та комбінувати з іншими компонентами.
Розглянемо сторінку панелі керування з такими областями:
основна область із посиланнями;
картка доходу;
панель активності;
окремий маршрут для сповіщень у панелі активності.
app/
├── layout.jsx
└── dashboard/
├── layout.jsx
├── page.jsx
├── default.jsx
├── @revenue/
│ ├── page.jsx
│ └── default.jsx
└── @activity/
├── page.jsx
├── default.jsx
└── notifications/
└── page.jsx// app/layout.jsx
export default function RootLayout({ children }) {
return (
<html lang="uk">
<body>{children}</body>
</html>
);
}// app/dashboard/layout.jsx
export default function DashboardLayout({
children,
revenue,
activity,
}) {
return (
<main>
<header>
<h1>Панель керування</h1>
</header>
<div
style={{
display: "grid",
gridTemplateColumns: "2fr 1fr",
gap: "24px",
}}
>
<section>
<h2>Основний вміст</h2>
{children}
</section>
<aside>
<section>
<h2>Доходи</h2>
{revenue}
</section>
<section>
<h2>Активність</h2>
{activity}
</section>
</aside>
</div>
</main>
);
}children// app/dashboard/page.jsx
import Link from "next/link";
export default function DashboardPage() {
return (
<div>
<p>Огляд панелі керування.</p>
<nav>
<Link href="/dashboard">Огляд</Link>
{" · "}
<Link href="/dashboard/notifications">
Сповіщення
</Link>
</nav>
</div>
);
}Цей компонент рендериться як prop children у DashboardLayout.
@revenue// app/dashboard/@revenue/page.jsx
export default function RevenuePage() {
return (
<article>
<strong>₴128 400</strong>
<p>Дохід за поточний місяць</p>
</article>
);
}@activity// app/dashboard/@activity/page.jsx
export default function ActivityPage() {
return (
<article>
<p>Остання активність:</p>
<ul>
<li>Оновлено профіль користувача</li>
<li>Створено нове замовлення</li>
</ul>
</article>
);
}За адресою /dashboard Next.js передасть до DashboardLayout:
children з app/dashboard/page.jsx;
revenue з app/dashboard/@revenue/page.jsx;
activity з app/dashboard/@activity/page.jsx.
Слот може мати власні вкладені маршрути:
app/
└── dashboard/
└── @activity/
├── page.jsx
└── notifications/
└── page.jsxФайл @activity/notifications/page.jsx відповідає URL:
/dashboard/notificationsСегмент @activity не з’являється в адресі.
// app/dashboard/@activity/notifications/page.jsx
export default function NotificationsPage() {
return (
<article>
<h3>Сповіщення</h3>
<ul>
<li>Нове замовлення очікує на обробку</li>
<li>Завершено синхронізацію даних</li>
</ul>
</article>
);
}Під час переходу на /dashboard/notifications:
слот activity показує сторінку сповіщень;
слот revenue не має маршруту notifications, тому використовує свій fallback;
children також не має маршруту notifications, тому використовує fallback.
default.jsxКоли користувач відкриває URL безпосередньо або оновлює сторінку, Next.js повинен визначити вміст кожного слота для поточного маршруту.
Якщо для слота немає відповідної сторінки, використовується default.jsx.
// app/dashboard/@revenue/default.jsx
export default function RevenueDefault() {
return (
<p>
Дані про доходи недоступні для цього маршруту.
</p>
);
}Fallback для неявного слота children розміщується без символу @:
// app/dashboard/default.jsx
export default function DashboardDefault() {
return (
<p>
Основний вміст для цього маршруту відсутній.
</p>
);
}Fallback для @activity:
// app/dashboard/@activity/default.jsx
export default function ActivityDefault() {
return (
<p>
Для цього маршруту немає активності.
</p>
);
}default.jsxПід час клієнтської навігації Next.js може зберігати поточний стан слота, якщо інший слот переходить на новий маршрут.
Під час повного завантаження сторінки або оновлення браузера стан потрібно відновити з URL. Якщо відповідного маршруту для слота немає, default.jsx визначає, що саме потрібно показати.
Без fallback конкретний слот може не мати коректного вмісту під час прямого відкриття URL.
Кожен слот може мати власні маршрути. Навігація до одного з них не означає, що всі області повинні змінити свій вміст.
У прикладі:
/dashboard
/dashboard/notificationsобласть активності змінюється з основного списку на сповіщення, а область доходів має власний fallback.
Це дозволяє будувати інтерфейси, у яких різні області реагують на один URL по-різному.
Наприклад, структура може виглядати так:
URL: /dashboard/notifications
children → dashboard/default.jsx
revenue → @revenue/default.jsx
activity → @activity/notifications/page.jsxПоточний маршрут визначається для кожного слота незалежно, але всі слоти відображаються одним layout.
Паралельні маршрути особливо корисні, коли layout відповідає за загальну композицію сторінки:
export default function WorkspaceLayout({
children,
navigation,
details,
}) {
return (
<div className="workspace">
<nav>{navigation}</nav>
<section>{children}</section>
<aside>{details}</aside>
</div>
);
}У цьому випадку:
navigation може містити власні вкладені маршрути;
details може змінюватися залежно від поточного URL;
children відповідає за центральну область;
сам layout залишається спільним.
@ не входить до URLДля директорії:
app/dashboard/@activity/notifications/page.jsxURL буде:
/dashboard/notificationsа не:
/dashboard/@activity/notificationslayoutЯкщо створити директорію @activity, але не додати activity до props і JSX у layout, її вміст не буде використано:
export default function DashboardLayout({ children }) {
return children;
}Правильний варіант:
export default function DashboardLayout({
children,
activity,
}) {
return (
<>
{children}
{activity}
</>
);
}Директорія:
app/dashboard/reports/створює звичайний URL:
/dashboard/reportsДиректорія:
app/dashboard/@reports/створює слот. Сегмент reports у цьому випадку не додається до URL.
Це основний сценарій паралельних маршрутів:
app/dashboard/page.jsx
app/dashboard/@revenue/page.jsx
app/dashboard/@activity/page.jsxУсі три сторінки відповідають URL /dashboard, але рендеряться в різних місцях layout.
Компоненти слотів за замовчуванням є Server Components, як і інші компоненти в app.
Тому вони можуть бути асинхронними та отримувати дані на сервері:
// app/dashboard/@revenue/page.jsx
async function getRevenue() {
return {
amount: 128400,
month: "поточний місяць",
};
}
export default async function RevenuePage() {
const revenue = await getRevenue();
return (
<article>
<strong>₴{revenue.amount.toLocaleString("uk-UA")}</strong>
<p>Дохід за {revenue.month}</p>
</article>
);
}Кожен слот може мати власну логіку отримання даних, але результат буде вставлений у спільний layout.
Для директорії @activity потрібно використовувати prop activity:
export default function Layout({ activities }) {
return activities;
}Це не відповідає імені слота.
Правильно:
export default function Layout({ activity }) {
return activity;
}Неправильно:
<Link href="/dashboard/activity">
Активність
</Link>Якщо activity — це слот @activity, його ім’я не є URL-сегментом.
Посилання має вести на реальний маршрут, наприклад:
<Link href="/dashboard/notifications">
Сповіщення
</Link>default.jsxЯкщо слот має вкладені маршрути, але для частини URL відповідної сторінки немає, потрібно передбачити fallback.
Наприклад, для @activity/notifications/page.jsx варто мати:
@activity/default.jsxІнакше під час прямого відкриття іншого маршруту слот може не мати вмісту.
Слот передається найближчому батьківському layout, який охоплює його директорію.
Наприклад:
app/
└── dashboard/
├── layout.jsx
└── @activity/
└── page.jsxСлот activity буде доступний у app/dashboard/layout.jsx, а не в кореневому layout, якщо він не знаходиться на відповідному рівні маршруту.
Слот — це спосіб організувати паралельний рендеринг, а не окремий префікс URL. Якщо потрібен URL /dashboard/activity, треба створити звичайний сегмент activity, а не покладатися лише на директорію @activity.
Паралельні маршрути дозволяють одночасно рендерити кілька областей інтерфейсу.
Слот створюється директорією з префіксом @, наприклад @activity.
Ім’я слота передається до layout як prop без символу @.
children — це неявний слот для звичайного вмісту маршруту.
Ім’я слота не додається до URL.
Кожен слот може мати власні вкладені маршрути.
default.jsx визначає fallback для слота, коли відповідного маршруту немає.
Паралельні маршрути підходять для панелей керування, бічних областей і складних макетів із незалежними частинами інтерфейсу.