Пошук уроків, статей та іншого контенту
Створите стани завантаження й помилок для маршрутів, щоб інтерфейс коректно реагував на асинхронні операції.
loading та error у Next.jsУ Next.js з App Router кожен сегмент маршруту може мати спеціальні файли:
loading.tsx — інтерфейс, який показується під час завантаження маршруту;
error.tsx — інтерфейс, який показується, якщо під час рендерингу маршруту виникла помилка.
Ці файли працюють на рівні конкретного сегмента маршруту. Наприклад, для маршруту /products структура може виглядати так:
app/
└── products/
├── page.tsx
├── loading.tsx
└── error.tsxУ результаті:
page.tsx відповідає за основний вміст;
loading.tsx показується, поки page.tsx очікує дані;
error.tsx показується, якщо page.tsx або дочірній компонент завершився помилкою.
Файл loading.tsx має експортувати React-компонент за замовчуванням:
export default function Loading() {
return <p>Завантаження товарів...</p>;
}Next.js автоматично використовує цей компонент під час очікування асинхронного вмісту маршруту.
app/products/page.tsx:
type Product = {
id: number;
title: string;
price: number;
};
async function getProducts(): Promise<Product[]> {
const response = await fetch("https://dummyjson.com/products", {
cache: "no-store",
});
if (!response.ok) {
throw new Error("Не вдалося завантажити товари");
}
const data: { products: Product[] } = await response.json();
return data.products;
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<main>
<h1>Товари</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
{product.title} — {product.price} $
</li>
))}
</ul>
</main>
);
}app/products/loading.tsx:
export default function Loading() {
return (
<main aria-busy="true">
<h1>Товари</h1>
<p>Завантаження списку товарів...</p>
</main>
);
}Коли користувач відкриває /products, Next.js може показати Loading, поки ProductsPage очікує результат fetch.
loading.tsx особливо корисний для:
повільних запитів до API;
переходів між сторінками;
відображення скелетона замість порожнього екрана;
повідомлення користувачу про поточний стан операції.
Файл error.tsx також розміщується всередині сегмента маршруту:
"use client";
type ErrorProps = {
error: Error & { digest?: string };
reset: () => void;
};
export default function Error({ error, reset }: ErrorProps) {
return (
<main>
<h1>Щось пішло не так</h1>
<p>Не вдалося завантажити товари.</p>
<button type="button" onClick={reset}>
Спробувати ще раз
</button>
</main>
);
}Компонент помилки повинен бути Client Component, тому на початку файлу додається директива:
"use client";error.tsxNext.js передає компоненту два основні значення:
error — об'єкт помилки;
reset — функція для повторного рендерингу сегмента маршруту.
Виклик reset() повторно запускає завантаження маршруту. Це корисно, якщо помилка була тимчасовою, наприклад через нестабільне мережеве з'єднання.
Структура файлів:
app/
└── products/
├── page.tsx
├── loading.tsx
└── error.tsxapp/products/page.tsx:
type Product = {
id: number;
title: string;
price: number;
};
type ProductsResponse = {
products: Product[];
};
async function getProducts(): Promise<Product[]> {
const response = await fetch("https://dummyjson.com/products", {
cache: "no-store",
});
if (!response.ok) {
throw new Error(`Помилка API: ${response.status}`);
}
const data: ProductsResponse = await response.json();
return data.products;
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<main>
<h1>Товари</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
<strong>{product.title}</strong>
<span> — {product.price} $</span>
</li>
))}
</ul>
</main>
);
}app/products/loading.tsx:
export default function Loading() {
return (
<main aria-busy="true">
<h1>Товари</h1>
<p>Завантаження...</p>
</main>
);
}app/products/error.tsx:
"use client";
type ErrorProps = {
error: Error & { digest?: string };
reset: () => void;
};
export default function ProductsError({ error, reset }: ErrorProps) {
return (
<main role="alert">
<h1>Не вдалося завантажити товари</h1>
<p>Перевірте з'єднання та спробуйте ще раз.</p>
<button type="button" onClick={reset}>
Повторити запит
</button>
{process.env.NODE_ENV === "development" && (
<pre>{error.message}</pre>
)}
</main>
);
}У режимі розробки цей приклад додатково показує повідомлення помилки. У production краще не виводити користувачу технічні деталі, оскільки вони можуть містити внутрішню інформацію про сервер або API.
error.tsx створює межу помилок для свого сегмента маршруту. Помилки з page.tsx і дочірніх сегментів можуть бути перехоплені найближчим error.tsx.
Наприклад:
app/
├── error.tsx
└── dashboard/
├── error.tsx
└── page.tsxЯкщо помилка виникла в /dashboard, її спочатку обробить app/dashboard/error.tsx. Якщо цього файла немає, Next.js використає найближчу доступну межу вище — app/error.tsx.
Це дає змогу створювати різні повідомлення для різних частин застосунку:
окрему помилку для каталогу;
окрему помилку для профілю;
загальну помилку для всього застосунку.
resetФункція reset не перезавантажує всю сторінку браузера. Вона просить Next.js повторно відрендерити сегмент маршруту, у якому сталася помилка.
<button type="button" onClick={() => reset()}>
Спробувати ще раз
</button>Це підходить для повторного виконання:
серверного fetch;
асинхронного отримання даних;
серверного компонента, який тимчасово завершився помилкою.
Якщо причина помилки не зникла, error.tsx буде показаний знову.
loading.tsx та error.tsx| Файл | Коли використовується | Тип компонента | |---|---|---| | loading.tsx | Поки маршрут очікує асинхронний результат | Server Component за замовчуванням | | error.tsx | Коли під час рендерингу маршруту виникла помилка | Client Component |
Важливо розрізняти помилку та звичайний порожній результат:
якщо API повернуло порожній масив, це не обов'язково помилка;
якщо API повернуло невдалий HTTP-статус, код може створити помилку через throw;
якщо дані ще не отримані, показується loading.tsx.
Наприклад, порожній список можна обробити в page.tsx:
export default async function ProductsPage() {
const products = await getProducts();
if (products.length === 0) {
return <p>Товарів поки немає.</p>;
}
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
);
}Такий стан не повинен використовувати error.tsx, оскільки запит завершився успішно.
"use client"error.tsx використовує reset і зазвичай містить обробники подій. Без директиви "use client" компонент не працюватиме як Client Component.
"use client";Директива має бути першим оператором у файлі.
fetch не вважає відповіді зі статусами 4xx або 5xx помилкою JavaScript автоматично. Потрібно перевірити response.ok і виконати throw:
const response = await fetch(url);
if (!response.ok) {
throw new Error("Запит завершився помилкою");
}Без цього page.tsx може спробувати обробити некоректну відповідь як успішну.
Не варто безумовно виводити error.message у production:
<p>{error.message}</p>Повідомлення може містити URL внутрішнього сервісу, SQL-подібні деталі або іншу службову інформацію. Краще показувати зрозуміле загальне повідомлення, а технічні дані залишати для журналів або режиму розробки.
error.tsx розміщено не в тому сегментіФайл помилки діє лише для свого сегмента та його дочірніх сегментів. Якщо він розміщений у app/products, він не оброблятиме помилки маршруту app/profile.
loading.tsx завжди буде помітнимДля швидкої операції стан завантаження може відображатися дуже короткий час або бути непомітним. Це нормальна поведінка. loading.tsx призначений для повільних асинхронних операцій і переходів, а не для гарантованого показу на фіксований час.
Показуйте в loading.tsx структуру, схожу на майбутній вміст сторінки.
Додавайте aria-busy="true" для області, яка завантажується.
Використовуйте role="alert" для повідомлення про помилку.
Пояснюйте користувачу, що сталося, без технічного жаргону.
Додавайте кнопку повторної спроби через reset.
Перевіряйте response.ok після серверного fetch.
Розміщуйте error.tsx на рівні того маршруту, для якого потрібне окреме повідомлення.
loading.tsx автоматично показує проміжний інтерфейс під час очікування маршруту.
error.tsx перехоплює помилки в межах сегмента маршруту.
error.tsx повинен бути Client Component.
Для повторного завантаження використовується функція reset.
fetch не створює JavaScript-помилку для невдалих HTTP-статусів автоматично — це потрібно зробити вручну.
Стан завантаження, помилки та порожнього результату — це різні стани, які слід обробляти окремо.