Обробка помилок та 404: ErrorBoundary і fallback-маршрути
Обробка помилок та 404: ErrorBoundary і fallback-маршрути
🎯 Мета лекції
- Створити сторінку 404 для неіснуючих маршрутів через catch-all маршрут.
- Обробляти помилки рендерингу через React ErrorBoundary.
- Реалізувати користувацький UX для помилкових станів.
- Забезпечити типобезпеку компонентів помилок у TypeScript.
- Інтегрувати логування помилок для моніторингу.
🔑 Ключові терміни
- 404 (Not Found): HTTP-статус, що вказує на відсутність запитуваного ресурсу.
- ErrorBoundary: React-компонент, що перехоплює помилки рендерингу у дочірніх компонентах.
- Catch-all Route: маршрут із wildcard (
*), що збігається з будь-яким невизначеним шляхом. - Fallback UI: запасний інтерфейс, що показується при помилці.
Проблема: неіснуючі маршрути
Користувач може ввести неіснуючий URL вручну або перейти за застарілим посиланням. Без обробки React Router просто не рендерить жодного маршруту — користувач бачить порожню сторінку:
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
{/* Що відбувається при URL /nonexistent? */}
</Routes>
URL /nonexistent не збігається з жодним маршрутом → порожня сторінка (або лише Layout, якщо він поза <Routes>).
Рішення: Catch-All маршрут для 404
Додайте маршрут із path="*" наприкінці списку маршрутів — він збігається з будь-яким невизначеним шляхом:
import { Routes, Route } from "react-router";
function NotFoundPage() {
return (
<div style={{ textAlign: "center", padding: "2rem" }}>
<h1>404 — Сторінку не знайдено</h1>
<p>Сторінка, яку ви шукаєте, не існує.</p>
<Link to="/">Повернутись на головну</Link>
</div>
);
}
function App() {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
{/* Catch-all маршрут — має бути останнім! */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
);
}
path="*" збігається з будь-яким URL, тому він завжди має бути останнім. Якщо розмістити його на початку, жоден інший маршрут не спрацює.✅ Правильно:* наприкінці.❌ Неправильно:
* на початку або в середині.Вкладені 404: catch-all у вкладених маршрутах
Catch-all маршрут можна використовувати всередині вкладених маршрутів для обробки невалідних підшляхів:
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
{/* Catch-all для невалідних підшляхів Dashboard */}
<Route path="*" element={<DashboardNotFound />} />
</Route>
{/* Глобальний catch-all */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
Поведінка:
/dashboard/profile→<Profile>/dashboard/invalid→<DashboardNotFound>(вкладений catch-all)/completely-wrong→<NotFoundPage>(глобальний catch-all)
function DashboardNotFound() {
return (
<div>
<h2>Сторінка Dashboard не знайдена</h2>
<p>Такої підсторінки в Dashboard не існує.</p>
<Link to="/dashboard">Повернутись до Dashboard</Link>
</div>
);
}
Редирект на 404 через useNavigate
Іноді потрібно програмно перенаправити на 404 — наприклад, коли завантаження даних повертає помилку:
import { useParams, useNavigate } from "react-router";
import { useEffect, useState } from "react";
type Product = {
id: string;
name: string;
price: number;
};
function ProductPage() {
const { productId } = useParams();
const navigate = useNavigate();
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadProduct() {
try {
const response = await fetch(`/api/products/${productId}`);
if (!response.ok) {
// Продукт не знайдено — редиректимо на 404
navigate("/404", { replace: true });
return;
}
const data = await response.json();
setProduct(data);
} catch (error) {
console.error("Помилка завантаження:", error);
navigate("/404", { replace: true });
} finally {
setLoading(false);
}
}
loadProduct();
}, [productId, navigate]);
if (loading) {
return <div>Завантаження...</div>;
}
if (!product) {
return null; // Редирект вже виконано
}
return (
<div>
<h1>{product.name}</h1>
<p>Ціна: {product.price} ₴</p>
</div>
);
}
<Route path="/404" element={<NotFoundPage />} />
<Route path="*" element={<NotFoundPage />} />
/404— явна сторінка помилки, на яку можна редиректити.*— catch-all для всіх невизначених URL.
/404 дозволяє програмно перенаправляти на нього.ErrorBoundary: обробка помилок рендерингу
ErrorBoundary — це React-компонент, що перехоплює помилки JavaScript у дочірніх компонентах під час рендерингу, у життєвому циклі та в конструкторах (але не в асинхронному коді чи обробниках подій).
React не надає вбудований ErrorBoundary для функціональних компонентів — потрібно використовувати класовий компонент або бібліотеку react-error-boundary.
Встановлення бібліотеки
npm install react-error-boundary
pnpm add react-error-boundary
Базове використання
import { ErrorBoundary } from "react-error-boundary";
function ErrorFallback({ error }: { error: Error }) {
return (
<div role="alert" style={{ padding: "2rem", border: "2px solid red" }}>
<h2>Щось пішло не так</h2>
<pre style={{ color: "red" }}>{error.message}</pre>
<button onClick={() => window.location.reload()}>
Перезавантажити сторінку
</button>
</div>
);
}
function App() {
return (
<BrowserRouter>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/broken" element={<BrokenComponent />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</ErrorBoundary>
</BrowserRouter>
);
}
// Компонент, що навмисно викидає помилку
function BrokenComponent() {
throw new Error("Цей компонент зламаний!");
return <div>Ви не побачите цей текст</div>;
}
ErrorBoundary:FallbackComponent: компонент, що рендериться при помилці. Отримує{ error, resetErrorBoundary }.fallback: React-елемент замість компонента (для простих випадків).onError: callback для логування помилок ((error, info) => { ... }).onReset: callback, що викликається при скиданні помилки.resetKeys: масив залежностей — при їх зміні ErrorBoundary автоматично скидається.
Локальні ErrorBoundary для окремих секцій
Замість одного глобального ErrorBoundary можна обгорнути окремі секції:
function App() {
return (
<BrowserRouter>
<Header />
<ErrorBoundary FallbackComponent={MainContentError}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</ErrorBoundary>
<ErrorBoundary FallbackComponent={SidebarError}>
<Sidebar />
</ErrorBoundary>
<Footer />
</BrowserRouter>
);
}
Якщо помилка виникне у <Dashboard>, зламається лише контент, а <Sidebar>, <Header> та <Footer> продовжать працювати.
- Глобальний ErrorBoundary для критичних помилок, що ламають весь застосунок.
- Локальні ErrorBoundary для ізольованих секцій (сайдбар, модальні вікна, віджети).
Логування помилок до Sentry / LogRocket
Для продакшн-моніторингу інтегруйте ErrorBoundary з сервісами логування:
import * as Sentry from "@sentry/react";
function App() {
return (
<BrowserRouter>
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, errorInfo) => {
// Логування до Sentry
Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack,
},
},
});
// Або власний сервіс
fetch("/api/log-error", {
method: "POST",
body: JSON.stringify({ error: error.message, stack: error.stack }),
});
}}
>
<Routes>{/* Маршрути */}</Routes>
</ErrorBoundary>
</BrowserRouter>
);
}
Типізація компонентів помилок
Для TypeScript створіть типізований fallback-компонент:
import { FallbackProps } from "react-error-boundary";
function TypedErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<h2>Виникла помилка</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Спробувати знову</button>
</div>
);
}
// Використання
<ErrorBoundary FallbackComponent={TypedErrorFallback}>
{/* Компоненти */}
</ErrorBoundary>
Практичне завдання
Створіть систему обробки помилок для застосунку:
- Глобальна сторінка 404 для неіснуючих маршрутів.
- Вкладена 404 для
/dashboard/*з контекстним повідомленням. - ErrorBoundary для секції Dashboard з fallback-компонентом.
- Компонент ProductPage, що редиректить на 404, якщо API повертає помилку.
- Кнопка "Повернутись" на всіх сторінках помилок.
import {
BrowserRouter,
Routes,
Route,
Link,
useNavigate,
useParams,
} from "react-router";
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
import { useEffect, useState } from "react";
// Глобальна сторінка 404
function NotFoundPage() {
const navigate = useNavigate();
return (
<div style={{ textAlign: "center", padding: "2rem" }}>
<h1>404 — Сторінку не знайдено</h1>
<p>Вибачте, сторінка, яку ви шукаєте, не існує.</p>
<button onClick={() => navigate(-1)}>← Повернутись назад</button>
{" або "}
<Link to="/">Перейти на головну</Link>
</div>
);
}
// Вкладена 404 для Dashboard
function DashboardNotFound() {
return (
<div style={{ padding: "2rem", border: "2px dashed orange" }}>
<h2>Розділ Dashboard не знайдено</h2>
<p>Такої підсторінки в Dashboard не існує.</p>
<Link to="/dashboard">← Повернутись до Dashboard</Link>
</div>
);
}
// Fallback для ErrorBoundary
function DashboardErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div
role="alert"
style={{ padding: "2rem", border: "2px solid red", backgroundColor: "#ffe0e0" }}
>
<h2>Помилка у Dashboard</h2>
<pre style={{ color: "red" }}>{error.message}</pre>
<button onClick={resetErrorBoundary}>Спробувати знову</button>
{" або "}
<Link to="/">Перейти на головну</Link>
</div>
);
}
// Layout для Dashboard
function DashboardLayout() {
return (
<div>
<h1>Dashboard</h1>
<nav>
<Link to="/dashboard">Головна</Link> |{" "}
<Link to="/dashboard/profile">Профіль</Link> |{" "}
<Link to="/dashboard/broken">Зламана сторінка</Link>
</nav>
<ErrorBoundary FallbackComponent={DashboardErrorFallback}>
<Outlet />
</ErrorBoundary>
</div>
);
}
function DashboardHome() {
return <div>Огляд Dashboard</div>;
}
function ProfilePage() {
return <div>Профіль користувача</div>;
}
// Компонент, що викидає помилку
function BrokenPage() {
throw new Error("Ця сторінка навмисно зламана для демонстрації ErrorBoundary!");
}
// Компонент ProductPage з редиректом на 404
function ProductPage() {
const { productId } = useParams();
const navigate = useNavigate();
const [product, setProduct] = useState<{ name: string } | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadProduct() {
try {
// Симуляція API (тільки "1", "2", "3" існують)
if (!["1", "2", "3"].includes(productId || "")) {
navigate("/404", { replace: true });
return;
}
setProduct({ name: `Продукт ${productId}` });
} catch {
navigate("/404", { replace: true });
} finally {
setLoading(false);
}
}
loadProduct();
}, [productId, navigate]);
if (loading) return <div>Завантаження продукту...</div>;
if (!product) return null;
return (
<div>
<h1>{product.name}</h1>
<p>Деталі продукту #{productId}</p>
<Link to="/products">← Назад до каталогу</Link>
</div>
);
}
function ProductsList() {
return (
<div>
<h1>Каталог продуктів</h1>
<ul>
<li><Link to="/products/1">Продукт 1</Link></li>
<li><Link to="/products/2">Продукт 2</Link></li>
<li><Link to="/products/3">Продукт 3</Link></li>
<li><Link to="/products/999">Неіснуючий продукт (→ 404)</Link></li>
</ul>
</div>
);
}
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<h1>Головна сторінка</h1>} />
<Route path="/products">
<Route index element={<ProductsList />} />
<Route path=":productId" element={<ProductPage />} />
</Route>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="profile" element={<ProfilePage />} />
<Route path="broken" element={<BrokenPage />} />
{/* Вкладена 404 */}
<Route path="*" element={<DashboardNotFound />} />
</Route>
{/* Іменований маршрут для програмного редиректу */}
<Route path="/404" element={<NotFoundPage />} />
{/* Глобальний catch-all */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
);
}
export default App;
Пояснення:
- Глобальна 404:
path="*"наприкінці відловлює всі невизначені URL. - Вкладена 404: у Dashboard окремий catch-all для контекстного повідомлення.
- ErrorBoundary: обгортає
<Outlet>у Dashboard — ловить помилки рендерингу. - ProductPage: перевіряє валідність ID, інакше редиректить на
/404. - Кнопки повернення:
navigate(-1)або<Link to="/">.
Підсумок
✅ Що ми опанували
- Створення сторінок 404 через catch-all маршрут
path="*". - Вкладені 404 для контекстних повідомлень про помилки.
- Обробку помилок рендерингу через
ErrorBoundary. - Локальні ErrorBoundary для ізоляції помилок у секціях.
- Програмний редирект на 404 при помилках завантаження даних.
- Інтеграцію логування помилок з моніторинговими сервісами.
🎯 Ключові практики
- Завжди додавайте catch-all маршрут наприкінці списку маршрутів.
- Використовуйте ErrorBoundary для критичних секцій застосунку.
- Логуйте помилки до Sentry / LogRocket у продакшні.
- Надавайте зрозумілі повідомлення про помилки з можливістю повернутись.
- Тестуйте обробку помилок — спеціально викидайте помилки у розробці.
Навігація та корисні хуки React Router
Програмна навігація через useNavigate, активні посилання через NavLink, робота з пошуковими параметрами через useSearchParams, відстеження локації через useLocation, інтеграція з історією браузера.
Data Router API: альтернативний синтаксис через createBrowserRouter
Повний довідник альтернативного об'єктного синтаксису React Router v7 через createBrowserRouter: від базових маршрутів до складних вкладених структур, динамічних параметрів, loaders, actions та обробки помилок.