Пошук уроків, статей та іншого контенту
Налаштуєте сторінки 404, обробку помилок завантаження та Error Boundary для зрозумілих повідомлень користувачу.
У застосунку на React помилки можуть виникати з різних причин:
користувач відкрив адресу, для якої немає маршруту;
завантаження даних завершилося помилкою;
сервер повернув статус 404, 500 або інший неуспішний статус;
під час рендерингу компонента виникла помилка JavaScript.
Для кожної ситуації варто показувати зрозуміле повідомлення замість порожнього екрана або технічного stack trace.
У застосунках із React Router зазвичай використовують:
окремий маршрут * для сторінки 404;
errorElement для помилок завантаження маршрутів і даних;
React Error Boundary для помилок під час рендерингу компонентів.
Маршрут із path="*" відповідає будь-якій адресі, яка не збіглася з іншими маршрутами:
import { Link } from "react-router-dom";
export function NotFoundPage() {
return (
<main>
<h1>404</h1>
<p>Сторінку не знайдено.</p>
<Link to="/">Повернутися на головну</Link>
</main>
);
}Такий маршрут потрібно розмістити після конкретних маршрутів:
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{
index: true,
element: <HomePage />,
},
{
path: "products/:id",
element: <ProductPage />,
},
{
path: "*",
element: <NotFoundPage />,
},
],
},
]);Маршрут * не обробляє помилки завантаження. Він лише показує сторінку, коли адреса не відповідає жодному відомому маршруту.
React Router дозволяє визначити loader для завантаження даних перед відображенням маршруту. Якщо loader викидає помилку, React Router передає її до найближчого errorElement.
async function productLoader({ params }) {
const response = await fetch(`/api/products/${params.id}`);
if (!response.ok) {
throw new Response("Товар не знайдено", {
status: response.status,
statusText: response.statusText,
});
}
return response.json();
}Для маршруту можна вказати компонент помилки:
{
path: "products/:id",
loader: productLoader,
element: <ProductPage />,
errorElement: <RouteErrorPage />,
}Якщо сервер поверне 404, користувач побачить RouteErrorPage, а не компонент ProductPage.
Хук useRouteError повертає помилку, яку передав React Router. Для помилок із HTTP-відповіддю зручно використовувати isRouteErrorResponse.
import {
isRouteErrorResponse,
Link,
useRouteError,
} from "react-router-dom";
export function RouteErrorPage() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
if (error.status === 404) {
return (
<main>
<h1>404</h1>
<p>Запитувані дані не знайдено.</p>
<Link to="/">На головну</Link>
</main>
);
}
return (
<main>
<h1>Помилка сервера</h1>
<p>
Не вдалося завантажити дані. Код помилки: {error.status}.
</p>
<Link to="/">На головну</Link>
</main>
);
}
return (
<main>
<h1>Щось пішло не так</h1>
<p>Спробуйте оновити сторінку пізніше.</p>
<Link to="/">На головну</Link>
</main>
);
}Користувачу не варто показувати error.stack, текст внутрішнього винятку або інші технічні деталі. Такі дані можна записати в систему моніторингу або консоль розробника.
Error Boundary — це спеціальний React-компонент, який перехоплює помилки під час:
рендерингу дочірніх компонентів;
виконання їхніх методів життєвого циклу;
побудови дерева компонентів.
Error Boundary реалізується класовим компонентом:
import React from "react";
export class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
};
}
static getDerivedStateFromError() {
return {
hasError: true,
};
}
componentDidCatch(error, errorInfo) {
// Тут можна передати помилку в систему моніторингу
console.error("Помилка рендерингу:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<main>
<h1>Не вдалося відобразити сторінку</h1>
<p>Спробуйте оновити сторінку або поверніться пізніше.</p>
</main>
);
}
return this.props.children;
}
}Boundary потрібно розмістити навколо частини інтерфейсу, для якої потрібен запасний варіант:
function ProductPage() {
return (
<ErrorBoundary>
<ProductDetails />
</ErrorBoundary>
);
}Якщо ProductDetails викине помилку під час рендерингу, замість нього відобразиться fallback-інтерфейс.
Один Boundary можна розмістити навколо всього застосунку. Але для великих сторінок корисно мати окремі межі для незалежних частин: наприклад, для основного контенту та бічної панелі. Тоді помилка в одному віджеті не обов’язково приховає всю сторінку.
Нижче наведено мінімальний застосунок із:
головною сторінкою;
сторінкою товару;
сторінкою 404;
помилкою завантаження товару;
errorElement;
React Error Boundary.
import React from "react";
import { createRoot } from "react-dom/client";
import {
createBrowserRouter,
Link,
Outlet,
RouterProvider,
isRouteErrorResponse,
useLoaderData,
useRouteError,
} from "react-router-dom";
const products = {
"1": {
id: "1",
name: "Механічна клавіатура",
description: "Клавіатура для тривалої роботи.",
},
"2": {
id: "2",
name: "Монітор",
description: "Монітор із високою роздільною здатністю.",
},
};
async function productLoader({ params }) {
await new Promise((resolve) => setTimeout(resolve, 300));
const product = products[params.id];
if (!product) {
throw new Response("Товар не знайдено", {
status: 404,
statusText: "Not Found",
});
}
return product;
}
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
};
}
static getDerivedStateFromError() {
return {
hasError: true,
};
}
componentDidCatch(error, errorInfo) {
// У production тут можна записати помилку в систему моніторингу
console.error("Помилка компонента:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<main>
<h1>Не вдалося відобразити товар</h1>
<p>Оновіть сторінку або спробуйте пізніше.</p>
<Link to="/">На головну</Link>
</main>
);
}
return this.props.children;
}
}
function Layout() {
return (
<>
<header>
<nav>
<Link to="/">Головна</Link>
</nav>
</header>
<Outlet />
</>
);
}
function HomePage() {
return (
<main>
<h1>Каталог товарів</h1>
<ul>
<li>
<Link to="/products/1">Механічна клавіатура</Link>
</li>
<li>
<Link to="/products/2">Монітор</Link>
</li>
<li>
<Link to="/products/999">Товар із помилкою завантаження</Link>
</li>
</ul>
</main>
);
}
function ProductPage() {
const product = useLoaderData();
return (
<ErrorBoundary>
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<Link to="/">Повернутися до каталогу</Link>
</main>
</ErrorBoundary>
);
}
function NotFoundPage() {
return (
<main>
<h1>404</h1>
<p>Такої сторінки не існує.</p>
<Link to="/">Повернутися на головну</Link>
</main>
);
}
function RouteErrorPage() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<main>
<h1>Не вдалося завантажити сторінку</h1>
<p>
Сервер повернув помилку {error.status}:{" "}
{error.statusText || "Невідома помилка"}.
</p>
<Link to="/">На головну</Link>
</main>
);
}
return (
<main>
<h1>Несподівана помилка</h1>
<p>Спробуйте оновити сторінку пізніше.</p>
<Link to="/">На головну</Link>
</main>
);
}
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
errorElement: <RouteErrorPage />,
children: [
{
index: true,
element: <HomePage />,
},
{
path: "products/:id",
loader: productLoader,
element: <ProductPage />,
},
{
path: "*",
element: <NotFoundPage />,
},
],
},
]);
createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
);У цьому прикладі:
/unknown відкриє NotFoundPage;
/products/1 відкриє сторінку товару;
/products/999 спричинить помилку в productLoader, яку обробить RouteErrorPage;
помилка під час рендерингу дочірнього компонента потрапить до ErrorBoundary.
*Використовується, коли URL не відповідає жодному відомому маршруту.
{
path: "*",
element: <NotFoundPage />,
}errorElementВикористовується React Router для помилок у маршруті, зокрема помилок loader і помилок рендерингу route-компонента, якщо їх не перехопив окремий Boundary.
{
path: "products/:id",
loader: productLoader,
element: <ProductPage />,
errorElement: <RouteErrorPage />,
}Використовується для ізоляції помилок під час рендерингу частини React-дерева.
<ErrorBoundary>
<ProductDetails />
</ErrorBoundary>Error Boundary не перехоплює помилки в обробниках подій, асинхронних callback-функціях або помилки на сервері. Помилки асинхронного завантаження потрібно обробляти там, де виконується запит, або через механізм loader і errorElement.
Клієнтський маршрут 404 працює лише після того, як JavaScript-застосунок завантажився. Якщо користувач напряму відкриє /products/1, вебсервер теж має повернути головний HTML-файл застосунку, а не власну серверну помилку 404.
Тому production-сервер для SPA зазвичай налаштовують так:
існуючі статичні файли віддаються напряму;
для інших адрес повертається index.html;
після запуску React Router визначає, який маршрут відобразити.
Це серверне налаштування доповнює, але не замінює маршрут path="*" у React.
Не відображайте на сторінці повний текст винятку або stack trace. Користувачу достатньо пояснити, що сталося, і запропонувати наступну дію.
*path="*" обробляє невідомі адреси, але не помилки в loader. Для помилок завантаження потрібен errorElement або власна обробка запиту.
fetch не викидає помилку автоматично для статусів 404 або 500. Потрібно перевіряти response.ok:
const response = await fetch("/api/products/1");
if (!response.ok) {
throw new Error("Не вдалося завантажити товар");
}
const product = await response.json();Якщо Error Boundary перехопив помилку, він має показати зрозумілий запасний інтерфейс. Порожній контейнер або технічне повідомлення не допомагають користувачу зрозуміти подальші дії.
Глобальний Boundary потрібен як останній рівень захисту, але локальні Boundary дозволяють зберегти інші частини інтерфейсу доступними.
Для невідомих URL створюйте маршрут із path="*".
Для помилок loader і завантаження даних використовуйте errorElement.
Перевіряйте response.ok, оскільки fetch не вважає HTTP 404 або 500 винятком.
Використовуйте useRouteError та isRouteErrorResponse, щоб показувати різні повідомлення для різних статусів.
React Error Boundary ізолює помилки під час рендерингу компонентів.
Повідомлення для користувача мають бути зрозумілими та не містити внутрішніх технічних деталей.
Для прямого відкриття клієнтських маршрутів production-сервер має повертати index.html.