Пошук уроків, статей та іншого контенту
Налаштуйте loading UI, потокову передачу HTML і миттєвий fallback під час завантаження сегментів маршруту.
У Next.js з App Router сторінка може складатися з кількох сегментів маршруту:
app/
├── layout.tsx
├── page.tsx
└── products/
├── layout.tsx
├── loading.tsx
└── page.tsxКоли користувач переходить на /products, Next.js може:
одразу показати доступний layout;
показати fallback із loading.tsx;
поступово передати готовий HTML;
замінити fallback повністю відрендереним вмістом.
Цей підхід називається streaming — потокова передача результату рендерингу частинами, а не очікування повної готовності сторінки.
loading.jsФайл loading.js, loading.jsx, loading.ts або loading.tsx створює автоматичний fallback для сегмента маршруту.
Наприклад:
app/
└── dashboard/
├── loading.tsx
└── page.tsxВміст app/dashboard/loading.tsx буде показано під час завантаження app/dashboard/page.tsx та дочірніх сегментів.
// app/dashboard/loading.tsx
export default function Loading() {
return (
<main>
<h1>Панель керування</h1>
<p>Завантаження даних...</p>
</main>
);
}Next.js концептуально обгортає сторінку в Suspense:
<Suspense fallback={<Loading />}>
<Page />
</Suspense>Це відбувається автоматично, тому зазвичай не потрібно вручну додавати Suspense лише для базового loading.tsx.
loading.tsxФайл діє для поточного сегмента маршруту та його дочірніх сторінок:
app/
└── dashboard/
├── loading.tsx
├── page.tsx
└── settings/
└── page.tsxapp/dashboard/loading.tsx може бути показаний під час завантаження:
/dashboard;
/dashboard/settings;
інших дочірніх маршрутів у цьому сегменті.
Loading UI не замінює layout. Спільний layout може залишатися на екрані, поки новий сегмент завантажується.
Без streaming серверу потрібно було б:
отримати всі дані;
відрендерити всю сторінку;
відправити повну HTML-відповідь браузеру.
Якщо одна частина сторінки повільна, користувач чекає на всю сторінку.
Під час streaming сервер може спочатку відправити:
layout;
навігацію;
заголовок;
fallback для повільної частини.
Після цього сервер передає готові частини сторінки, і React оновлює потрібні ділянки в браузері.
Переваги:
користувач швидше бачить інтерфейс;
повільні запити не блокують весь екран;
навігація відчувається миттєвішою;
можна окремо показувати skeleton для різних частин сторінки.
loading.tsxСтворимо сторінку товарів, яка навмисно очікує дані протягом двох секунд.
app/
└── products/
├── loading.tsx
└── page.tsx// app/products/loading.tsx
export default function Loading() {
return (
<main aria-busy="true">
<h1>Товари</h1>
<ul>
<li>Завантаження товару...</li>
<li>Завантаження товару...</li>
<li>Завантаження товару...</li>
</ul>
</main>
);
}// app/products/page.tsx
export const dynamic = 'force-dynamic';
type Product = {
id: number;
name: string;
price: number;
};
async function getProducts(): Promise<Product[]> {
// Імітуємо повільне отримання даних
await new Promise((resolve) => setTimeout(resolve, 2000));
return [
{ id: 1, name: 'Клавіатура', price: 2500 },
{ id: 2, name: 'Миша', price: 1200 },
{ id: 3, name: 'Навушники', price: 3200 },
];
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<main>
<h1>Товари</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name} — {product.price} грн
</li>
))}
</ul>
</main>
);
}Під час переходу на /products відбувається така послідовність:
Next.js починає рендерити сторінку.
getProducts() очікує дві секунди.
Користувач бачить app/products/loading.tsx.
Після отримання даних fallback замінюється списком товарів.
dynamic = 'force-dynamic' у цьому прикладі змушує сторінку рендеритися під час запиту. Це корисно для демонстрації поведінки повільного серверного запиту.
Suspenseloading.tsx корисний для всього сегмента маршруту. Але іноді потрібно, щоб заголовок і частина сторінки з'являлися одразу, а fallback показувався лише для повільного блоку.
Для цього можна використати Suspense вручну.
// app/reports/page.tsx
import { Suspense } from 'react';
export const dynamic = 'force-dynamic';
async function getReport() {
// Імітуємо повільне формування звіту
await new Promise((resolve) => setTimeout(resolve, 2500));
return {
totalOrders: 128,
totalRevenue: 456000,
};
}
async function ReportDetails() {
const report = await getReport();
return (
<section>
<h2>Результати</h2>
<p>Замовлень: {report.totalOrders}</p>
<p>Дохід: {report.totalRevenue} грн</p>
</section>
);
}
function ReportSkeleton() {
return (
<section aria-busy="true">
<h2>Результати</h2>
<p>Формування звіту...</p>
</section>
);
}
export default function ReportsPage() {
return (
<main>
<h1>Звіт</h1>
<p>Звіт формується на сервері.</p>
<Suspense fallback={<ReportSkeleton />}>
<ReportDetails />
</Suspense>
</main>
);
}Тут:
h1 і пояснення можна відобразити одразу;
ReportSkeleton показується лише для ReportDetails;
після завершення getReport() skeleton замінюється результатами;
інша частина сторінки не очікує завершення повільного блоку.
Такий підхід називають гранулярним streaming, оскільки межа очікування визначається для конкретної частини інтерфейсу.
loading.tsx і ручний SuspenseЦі механізми розв'язують різні задачі.
loading.tsxПідходить, коли потрібно показати fallback для всього сегмента:
app/
└── products/
├── loading.tsx
└── page.tsxЦе зручно для:
початкового завантаження маршруту;
переходу між сторінками;
простого loading UI;
fallback для всього вмісту сегмента.
SuspenseПідходить, коли сторінка має кілька незалежних частин:
<Suspense fallback={<ProductsSkeleton />}>
<Products />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>У такому випадку товари та статистика можуть завантажуватися незалежно. Готова частина відобразиться, не очікуючи на іншу.
Під час переходу між маршрутами через навігацію Next.js може показати loading.tsx нового сегмента.
Наприклад:
// app/layout.tsx
import Link from 'next/link';
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="uk">
<body>
<nav>
<Link href="/">Головна</Link>
<Link href="/products">Товари</Link>
<Link href="/reports">Звіти</Link>
</nav>
{children}
</body>
</html>
);
}Під час переходу з / на /products:
root layout залишається на місці;
посилання та навігація не зникають;
вміст сегмента products може бути тимчасово замінений loading.tsx;
після завершення рендерингу показується сторінка товарів.
Це краще за ручне відстеження стану завантаження для кожного переходу.
Fallback має бути схожим за структурою на майбутній контент.
Невдалий варіант:
export default function Loading() {
return <p>Завантаження...</p>;
}Такий fallback різко змінює розмітку після завантаження сторінки.
Кращий варіант:
export default function Loading() {
return (
<main aria-busy="true">
<div className="skeleton skeleton-title" />
<div className="skeleton skeleton-card" />
<div className="skeleton skeleton-card" />
</main>
);
}Корисні властивості loading UI:
зберігає приблизну структуру сторінки;
не створює різких стрибків layout;
має зрозумілий текст або візуальний skeleton;
повідомляє допоміжним технологіям про стан завантаження;
не містить інтерактивної логіки, якщо вона не потрібна.
aria-busy="true" повідомляє, що відповідна область ще завантажується. Після заміни fallback основним вмістом ця область більше не повинна мати значення true.
Потокова передача працює на сервері. Сервер починає відправляти результат, щойно доступна частина дерева React.
У браузері користувач отримує:
початковий HTML;
fallback для незавершених Suspense-меж;
додаткові фрагменти HTML;
службові дані, необхідні React для заміни fallback готовим контентом.
Розробнику не потрібно вручну керувати HTTP-потоком для стандартного використання loading.tsx або Suspense у Server Components.
Next.js розпізнає спеціальний файл саме з назвою:
loading.tsxФайли loader.tsx, pending.tsx або loading-ui.tsx автоматично Loading UI не створюють.
Якщо файл знаходиться тут:
app/dashboard/loading.tsxвін не буде fallback для незалежного маршруту:
app/settings/page.tsxДля /settings потрібен власний app/settings/loading.tsx або fallback на рівні спільного батьківського сегмента.
SuspenseЯкщо кілька повільних компонентів очікуються до повернення всієї сторінки, користувач не побачить окремі частини раніше.
Коли блоки незалежні, обгорніть їх у різні Suspense:
<Suspense fallback={<ProductsSkeleton />}>
<Products />
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<Orders />
</Suspense>Якщо fallback має зовсім іншу висоту або структуру, після завантаження сторінка може стрибнути. Краще повторити основні області майбутнього інтерфейсу.
loading.tsx як стан локальної діїloading.tsx призначений для завантаження сегмента маршруту. Він не є універсальним станом для:
натискання кнопки;
відправлення форми;
локального клієнтського запиту;
завантаження окремого віджета після кліку.
Для локальних інтерактивних станів використовують відповідну логіку клієнтського компонента. loading.tsx залишається механізмом завантаження маршруту та його серверного дерева.
Server Components можуть очікувати дані безпосередньо:
export default async function Page() {
const data = await getData();
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}Не додавайте 'use client' до сторінки без потреби. Це може ускладнити серверний рендеринг і змінити спосіб отримання даних.
loading.tsx створює автоматичний fallback для сегмента маршруту.
Loading UI показується під час початкового завантаження та навігації до сегмента.
Streaming дає змогу передавати HTML і результат рендерингу частинами.
Спільні layouts можуть залишатися на екрані, поки завантажується новий сегмент.
Ручний Suspense дає змогу створити fallback для окремої частини сторінки.
Кілька Suspense-меж дозволяють незалежним блокам завантажуватися окремо.
Якісний fallback має відповідати структурі майбутнього контенту та не створювати різких змін інтерфейсу.