Как обрабатывать ошибки на уровне компонентов? Как отлавливать ошибки в рендере?
Обработка ошибок на уровне компонентов в React — это критически важная часть разработки устойчивых интерфейсов. Ошибки могут возникнуть в рендеринге, методах жизненного цикла, асинхронных вызовах, событиях, хуках и других частях UI. React предоставляет встроенные и расширяемые способы отлавливать, логировать и реагировать на такие ошибки, чтобы не приводить к падению всего приложения.
1. Ошибки в рендеринге и жизненном цикле
С версии React 16 появилась концепция Error Boundaries (граничные обработчики ошибок). Они перехватывают ошибки:
-
в render()
-
в constructor()
-
в componentDidMount(), componentDidUpdate(), componentWillUnmount()
-
в методах дочерних компонентов
Они не ловят ошибки:
-
в обработчиках событий (onClick, и т.п.)
-
в асинхронных функциях (setTimeout, fetch, Promise)
-
в серверном рендеринге (SSR)
-
в хуках (useEffect, useMemo, и т.п.)
2. Реализация Error Boundary
Для использования Error Boundary необходимо создать класс-компонент, реализующий методы componentDidCatch и getDerivedStateFromError.
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error }; // обновит состояние
}
componentDidCatch(error, errorInfo) {
console.error('Ошибка в компоненте:', error, errorInfo);
// можно отправить лог на сервер
}
render() {
if (this.state.hasError) {
return <h2>Что-то пошло не так.</h2>;
}
return this.props.children;
}
}
Применение:
<ErrorBoundary>
<ComponentWithRiskyRender />
</ErrorBoundary>
Можно оборачивать как всё приложение, так и отдельные части (например, каждая вкладка, карточка или виджет).
3. Использование нескольких Error Boundaries
Можно создать разные обработчики для разных частей UI, чтобы ошибка в одной области не приводила к краху всего приложения:
<Header />
<ErrorBoundary>
<Dashboard />
</ErrorBoundary>
<ErrorBoundary>
<Notifications />
</ErrorBoundary>
<Footer />
Такой подход позволяет изолировать ошибки и показывать локальные fallback UI.
4. Error Boundary с кастомным интерфейсом
class CustomErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
}
static getDerivedStateFromError(error) {
return { error };
}
componentDidCatch(error, info) {
// логирование
}
render() {
const { error } = this.state;
if (error) {
return (
<div className="error-ui">
<h1>Ошибка</h1>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>Перезагрузить</button>
</div>
);
}
return this.props.children;
}
}
5. Ошибки в хуках и функциональных компонентах
React не позволяет использовать componentDidCatch в функциональных компонентах напрямую. Но с помощью библиотеки react-error-boundary можно реализовать Error Boundary и в функциональном стиле.
Пример с react-error-boundary:
npm install react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';
function FallbackComponent({ error, resetErrorBoundary }) {
return (
<div>
<p>Ошибка: {error.message}</p>
<button onClick={resetErrorBoundary}>Попробовать снова</button>
</div>
);
}
function App() {
return (
<ErrorBoundary
FallbackComponent={FallbackComponent}
onReset={() => {
// сбросить состояние или выполнить перенаправление
}}
\>
<RiskyComponent />
</ErrorBoundary>
);
}
6. Ошибки в событиях и Promise
Ошибки внутри событий (например, onClick) не перехватываются Error Boundary. Их нужно ловить вручную через try/catch.
function handleClick() {
try {
// рискованная логика
} catch (error) {
console.error('Ошибка в обработчике события:', error);
}
}
То же касается fetch и других Promise:
async function loadData() {
try {
const res = await fetch('/api/data');
const json = await res.json();
setData(json);
} catch (error) {
console.error('Ошибка загрузки:', error);
setError(error);
}
}
7. Обработка ошибок в хуках (useEffect, useCallback, useMemo)
Асинхронные функции внутри хуков нужно защищать try/catch:
useEffect(() => {
const load = async () => {
try {
const res = await fetch('/api/data');
const json = await res.json();
setData(json);
} catch (e) {
setError(e);
}
};
load();
}, \[\]);
Также можно использовать кастомный хук useSafeAsync или библиотеку типа SWR, React Query — они уже имеют встроенные механизмы обработки ошибок.
8. Логирование ошибок
Ошибки полезно не только отображать, но и логировать на сервер:
componentDidCatch(error, errorInfo) {
sendToMonitoringService({
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack
});
}
Можно интегрировать такие сервисы, как:
-
Sentry
-
Bugsnag
-
LogRocket
-
Datadog
-
Rollbar
Они позволяют собирать подробные отчёты об ошибках, включая состояние компонентов, контекст, действия пользователя и пр.
9. Условный рендеринг и защита от невалидных данных
Иногда ошибки вызваны не исключениями, а невалидными данными (например, undefined, null и т.п.). В таких случаях стоит использовать защитные проверки:
if (!user) return <Loading />;
return <UserProfile data={user} />;
Или с помощью опциональной цепочки:
<p>{user?.name}</p>
10. Ошибки при SSR и Hydration
При серверной генерации (Next.js, Remix) ошибки могут возникнуть до загрузки JS. Для этого часто используют getInitialProps, getServerSideProps и другие серверные обработчики, где нужно обязательно отлавливать ошибки.
Также важно отлавливать ошибки при hydrate() — если markup отличается между сервером и клиентом, React выдаст предупреждение.
11. Интеграция с системами трекинга ошибок
Библиотеки типа Sentry могут оборачивать Error Boundaries:
import \* as Sentry from '@sentry/react';
const SentryBoundary = Sentry.ErrorBoundary;
<SentryBoundary fallback={<p>Произошла ошибка</p>}>
<ComponentWithRisk />
</SentryBoundary>
Также они могут автоматически перехватывать ошибки из window.onerror, unhandledrejection и других глобальных источников.
Грамотное управление ошибками на уровне компонентов — это не только Error Boundaries, но и осознанное проектирование: валидация данных, защита асинхронных операций, логирование и fallback UI. Чем изолированнее и предсказуемее ведёт себя каждый компонент при сбое, тем стабильнее работает всё приложение.