Пошук уроків, статей та іншого контенту
Показуйте кілька незалежних UI-сегментів одночасно за допомогою Parallel Routes і окремих станів завантаження.
Parallel Routes в App Router дають змогу рендерити кілька незалежних UI-сегментів у межах одного layout.
Наприклад, сторінка панелі керування може одночасно містити:
основний вміст;
аналітику;
список команди;
сповіщення.
Кожен сегмент може:
мати власний маршрут;
завантажувати дані незалежно;
показувати власний стан завантаження;
зберігати свій активний стан під час навігації.
Parallel Routes позначаються спеціальними папками зі знаком @:
app/
└── dashboard/
├── layout.tsx
├── page.tsx
├── @analytics/
│ ├── page.tsx
│ └── loading.tsx
└── @team/
├── page.tsx
└── loading.tsxНазви @analytics і @team називаються слотами.
Слоти не додаються до URL. Наприклад:
app/dashboard/@analytics/page.tsx;
app/dashboard/@team/page.tsx;
обидва сегменти відображаються на URL /dashboard.
Layout отримує кожен слот як окремий prop. Звичайний children також є слотом за замовчуванням.
// app/dashboard/layout.tsx
import type { ReactNode } from "react";
type DashboardLayoutProps = {
children: ReactNode;
analytics: ReactNode;
team: ReactNode;
};
export default function DashboardLayout({
children,
analytics,
team,
}: DashboardLayoutProps) {
return (
<div>
<header>
<h1>Панель керування</h1>
</header>
<main>
<section>{children}</section>
<aside>
<div>{analytics}</div>
<div>{team}</div>
</aside>
</main>
</div>
);
}У цьому прикладі:
children відповідає app/dashboard/page.tsx;
analytics відповідає app/dashboard/@analytics/page.tsx;
team відповідає app/dashboard/@team/page.tsx.
Якщо слот не передати в layout, Next.js не зможе використати його в цьому layout.
Кожен слот може мати власний loading.tsx. Це дає змогу показувати fallback саме для того сегмента, який очікує на дані.
app/
└── dashboard/
├── layout.tsx
├── page.tsx
├── @analytics/
│ ├── page.tsx
│ └── loading.tsx
└── @team/
├── page.tsx
└── loading.tsxЯкщо аналітика завантажується довше за список команди:
слот analytics покаже свій loading UI;
слот team уже покаже готовий вміст;
основний вміст сторінки залишиться доступним.
Це відрізняється від одного спільного loading UI для всієї сторінки, який блокував би всі сегменти одночасно.
Структура файлів:
app/
└── dashboard/
├── layout.tsx
├── page.tsx
├── @analytics/
│ ├── page.tsx
│ └── loading.tsx
└── @team/
├── page.tsx
└── loading.tsx// app/dashboard/layout.tsx
import type { ReactNode } from "react";
type DashboardLayoutProps = {
children: ReactNode;
analytics: ReactNode;
team: ReactNode;
};
export default function DashboardLayout({
children,
analytics,
team,
}: DashboardLayoutProps) {
return (
<div
style={{
maxWidth: 1000,
margin: "0 auto",
padding: 24,
fontFamily: "sans-serif",
}}
>
<header style={{ marginBottom: 24 }}>
<h1>Панель керування</h1>
</header>
<div
style={{
display: "grid",
gridTemplateColumns: "2fr 1fr",
gap: 24,
}}
>
<main>{children}</main>
<aside style={{ display: "grid", gap: 16 }}>
<section
style={{
border: "1px solid #ddd",
borderRadius: 8,
padding: 16,
}}
>
{analytics}
</section>
<section
style={{
border: "1px solid #ddd",
borderRadius: 8,
padding: 16,
}}
>
{team}
</section>
</aside>
</div>
</div>
);
}// app/dashboard/page.tsx
export default function DashboardPage() {
return (
<section>
<h2>Огляд</h2>
<p>
Це основний вміст панелі. Він рендериться через слот children.
</p>
</section>
);
}// app/dashboard/@analytics/page.tsx
async function getAnalytics() {
// Імітуємо повільне отримання даних із сервера
await new Promise((resolve) => setTimeout(resolve, 1800));
return {
revenue: 12840,
orders: 342,
};
}
export default async function AnalyticsPage() {
const analytics = await getAnalytics();
return (
<div>
<h2>Аналітика</h2>
<p>Виручка: {analytics.revenue} €</p>
<p>Замовлення: {analytics.orders}</p>
</div>
);
}// app/dashboard/@analytics/loading.tsx
export default function AnalyticsLoading() {
return (
<div aria-busy="true">
<h2>Аналітика</h2>
<p>Завантаження статистики...</p>
</div>
);
}// app/dashboard/@team/page.tsx
async function getTeam() {
// Імітуємо незалежне отримання даних
await new Promise((resolve) => setTimeout(resolve, 700));
return ["Олена", "Андрій", "Марія"];
}
export default async function TeamPage() {
const team = await getTeam();
return (
<div>
<h2>Команда</h2>
<ul>
{team.map((member) => (
<li key={member}>{member}</li>
))}
</ul>
</div>
);
}// app/dashboard/@team/loading.tsx
export default function TeamLoading() {
return (
<div aria-busy="true">
<h2>Команда</h2>
<p>Завантаження учасників...</p>
</div>
);
}Після переходу на /dashboard Next.js почне рендерити всі сегменти паралельно. Очікуваний порядок відображення:
одразу відобразиться основний вміст;
для аналітики з'явиться AnalyticsLoading;
для команди з'явиться TeamLoading;
команда стане доступною приблизно через 700 мс;
аналітика стане доступною приблизно через 1800 мс.
Слот може мати власні вкладені маршрути. Наприклад:
app/
└── dashboard/
└── @analytics/
├── page.tsx
├── loading.tsx
└── details/
└── page.tsxФайл @analytics/details/page.tsx відповідає маршруту /dashboard/details, але його вміст буде відображений саме в слоті analytics.
Знак @analytics не з'являється в URL. Він лише визначає, в який prop layout потрапить результат рендерингу.
default.tsxПід час навігації Next.js може зберігати поточний активний стан слота. Наприклад, користувач відкрив підсторінку в одному слоті, а потім перейшов на інший маршрут. У такому випадку слот може продовжити показувати попередній вміст.
Під час повного завантаження URL безпосередньо браузером попередній стан відновити неможливо. Для такого випадку слот може мати default.tsx.
app/
└── dashboard/
└── @analytics/
├── page.tsx
├── default.tsx
└── details/
└── page.tsx// app/dashboard/@analytics/default.tsx
export default function AnalyticsDefault() {
return null;
}default.tsx визначає, що показати, якщо для слота немає відповідного активного маршруту. Це може бути:
null;
заглушка;
базовий стан сегмента;
повідомлення про відсутність даних.
Наприклад:
// app/dashboard/@team/default.tsx
export default function TeamDefault() {
return <p>Виберіть розділ команди.</p>;
}default.tsx не замінює loading.tsx:
loading.tsx показується, поки сегмент завантажується;
default.tsx визначає початковий або запасний стан маршруту.
Parallel Routes особливо корисні для інтерфейсів, у яких кілька частин сторінки мають власну навігацію.
Наприклад, у dashboard можна мати:
основну область зі сторінками звітів;
бічну область зі статистикою;
область команди;
окремий сегмент сповіщень.
Під час клієнтської навігації Next.js може оновити лише той слот, маршрут якого змінився. Інші слоти при цьому не обов'язково перерендерюються або втрачають свій активний стан.
Це дозволяє будувати інтерфейси на кшталт:
вкладених панелей;
багатоколонкових dashboard;
поштових клієнтів;
редакторів із панеллю властивостей;
модальних маршрутів.
Важливо, що поведінка залежить від типу навігації:
під час клієнтської навігації Next.js може зберегти активний стан слотів;
під час повного перезавантаження сторінки стан визначається маршрутами та default.tsx.
Parallel Routes доречні, коли кілька сегментів:
видимі одночасно;
мають різні джерела даних;
повинні завантажуватися незалежно;
мають власні loading UI;
можуть змінюватися окремо один від одного.
Якщо сторінка має лише один основний вміст і один загальний стан завантаження, звичайного page.tsx та loading.tsx буде достатньо.
Папка @analytics сама по собі не гарантує відображення компонента. Layout повинен прийняти prop і вставити його в JSX:
export default function Layout({
children,
analytics,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<div>
{children}
{analytics}
</div>
);
}@slot з'явиться в URL@analytics — це внутрішнє ім'я слота. Воно не є частиною URL.
Файл:
app/dashboard/@analytics/page.tsxне створює URL /dashboard/analytics. Він рендериться в слоті analytics для /dashboard.
Якщо loading.tsx розташувати лише на рівні app/dashboard/loading.tsx, він не дасть кожному слоту незалежний fallback.
Для окремих станів завантаження loading.tsx потрібно розташувати всередині відповідного слота:
app/dashboard/@analytics/loading.tsx
app/dashboard/@team/loading.tsxdefault.tsx для складного маршрутуЯкщо слот має кілька вкладених маршрутів і повинен мати чіткий початковий стан під час прямого відкриття URL, додайте default.tsx.
Без нього слот може не мати коректного вмісту для ситуації, коли активний маршрут не відповідає жодній його сторінці.
Слот — це не новий незалежний URL-рівень. Він передає React-вміст у батьківський layout через prop.
Саме layout вирішує:
де розмістити слот;
як його стилізувати;
чи показувати його завжди;
у якій послідовності розташувати сегменти.
Parallel Routes дозволяють рендерити кілька UI-сегментів одночасно.
Слоти створюються папками з префіксом @.
Слоти передаються в layout як props.
children є стандартним слотом за замовчуванням.
Кожен слот може мати власний page.tsx і loading.tsx.
Слоти не додаються до URL.
default.tsx визначає запасний стан слота під час прямого завантаження або відсутності активного маршруту.
Parallel Routes підходять для dashboard та інших інтерфейсів із незалежними областями, які повинні завантажуватися паралельно.