Границі помилок у React
Границі помилок (Error boundaries) - це класові компоненти React, які перехоплюють помилки JavaScript у дочірньому дереві під час рендерингу, в lifecycle-методах та конструкторах, після чого показують запасний інтерфейс і логують помилку.
Теорія
TL;DR
- Аналогія: автоматичний вимикач у щитку. Коротке замикання в одному відгалуженні вибиває автомат, але решта квартири має світло.
- Без boundary одна помилка у компоненті розмонтовує все дерево React. З boundary падає лише той піддеревець.
getDerivedStateFromErrorоновлює стан для запасного UI (синхронно, під час рендеру).componentDidCatchпотрібен для логування (після commit-фази).- Boundaries перехоплюють лише помилки фази рендерингу в нащадках. Обробники подій та async-код потребують
try/catch. - Обгортай будь-який піддеревець, що може падати незалежно: сторонні віджети, панелі дашборду, lazy-маршрути.
Швидкий приклад
class ErrorBoundary extends React.Component {
state = { hasError: false };
// Виконується під час рендеру - повертає новий стан для fallback
static getDerivedStateFromError(error) {
return { hasError: true };
}
// Виконується після commit - тут логуємо у Sentry
componentDidCatch(error, info) {
console.error('Caught:', error, info.componentStack);
}
render() {
if (this.state.hasError) return <h1>Щось зламалось!</h1>;
return this.props.children;
}
}
// Обгортаємо компонент, який може кинути помилку
<ErrorBoundary>
<BuggyCounter /> {/* кидає помилку під час рендеру */}
</ErrorBoundary>
// Результат: "Щось зламалось!" замість білого екрануgetDerivedStateFromError спрацьовує під час фази рендеру, тому fallback з'являється одразу. componentDidCatch спрацьовує після того як React закомітив fallback у DOM.
Головна різниця
До React 16 помилка рендерингу в будь-якому компоненті залишала DOM у зламаному стані без чистого шляху відновлення. React 16 приніс fiber-архітектуру з підтримкою error boundaries: коли компонент кидає помилку під час рендеру, React обходить fiber-дерево вгору в пошуках найближчого класового компонента з getDerivedStateFromError. Той компонент рендерить fallback. Сусідні піддерева не зачіпаються.
Коли використовувати
- Сторонні віджети або embed-и: обгортай кожен окремо, щоб падіння вендорного скрипту не клало всю сторінку.
- Панелі дашборду: якщо API одного графіка повертає сміття під час рендеру, решта панелей продовжує працювати.
- Lazy-маршрути: невдалий динамічний імпорт не обнуляє весь застосунок.
- Глобальний fallback: один boundary на рівні застосунку як запасний варіант, з кнопкою перезавантаження та логуванням у Sentry.
- Не тут: обробники подій,
setTimeout,fetch. Для них потрібенtry/catchабо.catch().
Як React обробляє помилки всередині
Reconciler React відстежує помилки під час фази рендерингу. Коли компонент кидає виняток, він піднімається вгору по fiber-вузлах, поки не натрапить на класовий компонент з getDerivedStateFromError. Цей метод статичний і синхронний: React отримує новий стан одразу і рендерить fallback в тому ж проходженні. Після того як fallback закомічено в DOM, запускається componentDidCatch для побічних ефектів: логування, відправка у Sentry, оновлення метрик.
Я завжди нагадую колегам: якщо власний render boundary кидає помилку, вона не перехоплюється самим boundary. Вона піднімається до наступного boundary вгору по дереву. Тримай fallback-рендер максимально простим.
Типові помилки
1. Очікування перехоплення помилок з обробників подій
// НЕ перехоплюється - обробники подій виконуються поза render-циклом React
<ErrorBoundary>
<button onClick={() => { throw new Error('boom'); }}>Натисни</button>
</ErrorBoundary>Використовуй try/catch безпосередньо всередині обробника.
2. Пропустити getDerivedStateFromError і покластися тільки на componentDidCatch
// Неправильно - setState тут ставить у чергу ще один рендер-цикл, fallback з'являється із затримкою
componentDidCatch(error, info) {
this.setState({ hasError: true });
}Додай static getDerivedStateFromError, щоб fallback рендерився в тому ж проходженні що й помилка.
3. Спроба написати boundary як функціональний компонент
// Не працює
function BadBoundary({ children }) {
const [hasError, setHasError] = useState(false);
// немає lifecycle-методу для перехоплення помилок рендеру
}Тільки класові компоненти можуть бути error boundaries. Для проектів на функціональних компонентах використовуй бібліотеку react-error-boundary.
4. Складна логіка у власному render boundary
Якщо render boundary кидає виняток, він не перехоплюється тим самим boundary. Він піде до наступного boundary вгору. Тримай fallback JSX статичним і простим: без запитів до API, без складних умов окрім перевірки hasError.
Реальне використання
- Create React App: обгортає кореневий
<App>уErrorBoundaryза замовчуванням, починаючи з React 16.13. - Next.js:
_error.js(pages router) обробляє клієнтські помилки на рівні застосунку; app router використовуєerror.jsна рівні сегменту маршруту. - Sentry: постачає
<Sentry.ErrorBoundary>з вбудованимfallbackпропом і автоматичним перехопленням помилок. - Redux Toolkit: команди часто обгортають lazy-завантажені feature slices в окремі boundaries.
- Storybook: boundaries на рівні кожної story запобігають тому, щоб одна зламана story клала весь dev-інструмент.
Питання на співбесіді
Q: Які фази React перехоплює error boundary?
A: Фазу рендерингу, lifecycle-методи та конструктори нащадків. Не перехоплює: обробники подій, async-код (setTimeout, Promise), SSR та власний render boundary.
Q: Яка різниця між getDerivedStateFromError і componentDidCatch?
A: getDerivedStateFromError статичний і синхронний. Запускається під час фази рендеру і повертає новий стан, щоб fallback з'явився одразу. componentDidCatch запускається після commit-фази і призначений для побічних ефектів: логування, відправки у Sentry, оновлення метрик.
Q: Як скинути error boundary після спрацьовування?
A: Викликати setState({ hasError: false }) з кнопки retry у fallback UI. Це змусить boundary знову рендерити дочірні компоненти. Деякі команди також відстежують зміни пропів у componentDidUpdate для автоматичного скидання.
Q: Чи є хуковий аналог?
A: Офіційного хука немає. Використовуй react-error-boundary, яка обгортає класовий boundary і надає зручний функціональний API, включно з useErrorBoundary для ручного тригерингу з async-коду.
Q: Як boundaries поводяться в concurrent React 18 з transitions?
A: Помилки в startTransition розгортаються до найближчого boundary без переривання поточного UI. React може повторити transition окремо. Механізм fiber unwinding гарантує відсутність "зомбі-нащадків" у дереві. Варто згадати це на senior-рівні.
Приклади
Базовий: перехоплення помилки рендерингу
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error(error, info.componentStack);
}
render() {
if (this.state.hasError) return <p>Не вдалося завантажити цю секцію.</p>;
return this.props.children;
}
}
function BuggyComponent() {
throw new Error('render crash'); // кидає при кожному рендері
}
<div>
<ErrorBoundary>
<BuggyComponent /> {/* показує fallback */}
</ErrorBoundary>
<p>Цей абзац рендериться нормально.</p>
</div>Fallback показується всередині boundary. Сусідній абзац не зачіпається, бо boundaries ізолюють падіння у свій піддеревець.
Середній рівень: панель дашборду з retry і Sentry
class ChartPanelBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
Sentry.captureException(error, { extra: errorInfo });
}
render() {
if (this.state.hasError) {
return (
<div className="error-panel">
<h3>Графік не завантажився</h3>
<button
onClick={() => this.setState({ hasError: false, error: null })}
>
Спробувати ще
</button>
</div>
);
}
return this.props.children;
}
}
function Dashboard() {
return (
<div>
<ChartPanelBoundary>
<RevenueChart dataUrl="/api/revenue" />
</ChartPanelBoundary>
<ChartPanelBoundary>
<UsersChart dataUrl="/api/users" />
</ChartPanelBoundary>
</div>
);
}
// Якщо RevenueChart падає, UsersChart продовжує працювати
// Кнопка retry виставляє hasError: false і рендерить дочірні компоненти зановоКожна панель має свій boundary. Кнопка retry викликає setState для очищення hasError, і boundary рендерить дочірні компоненти заново.
Складний рівень: що boundaries не перехоплюють
// 1. Async-помилка - жоден boundary її не перехопить
function AsyncProblem() {
useEffect(() => {
setTimeout(() => {
throw new Error('async boom'); // boundary пропустить це
}, 1000);
}, []);
return <p>Завантажено</p>;
}
// 2. Помилка в обробнику події - також не перехоплюється
function ClickProblem() {
return (
<button onClick={() => { throw new Error('click boom'); }}>
Натисни мене
</button>
);
}
// Правильний підхід для async-помилок
function AsyncFixed() {
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.catch(err => setError(err.message)); // обробляємо вручну
}, []);
if (error) return <p>Помилка: {error}</p>;
return <p>Завантажено</p>;
}Обгортання цих компонентів у <ErrorBoundary> нічого не дасть для async або обробників подій. Їх потрібно обробляти через try/catch або .catch() безпосередньо на місці виклику.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.