Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Границі помилок у React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Границі помилок (Error boundaries)** - це класові компоненти, які перехоплюють помилки JavaScript у дочірньому дереві під час рендерингу і показують запасний UI замість краш застосунку. ```jsx class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } render() { if (this.state.hasError) return <h1>Щось зламалось!</h1>; return this.props.children; } } ``` **Головне:** тільки класові компоненти можуть бути boundaries. Обробники подій та async-код потребують `try/catch`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Границі помилок (Error boundaries)** - це класові компоненти React, які перехоплюють помилки JavaScript у дочірньому дереві під час рендерингу, в lifecycle-методах та конструкторах, після чого показують запасний інтерфейс і логують помилку. ## Теорія ### TL;DR - Аналогія: автоматичний вимикач у щитку. Коротке замикання в одному відгалуженні вибиває автомат, але решта квартири має світло. - Без boundary одна помилка у компоненті розмонтовує все дерево React. З boundary падає лише той піддеревець. - `getDerivedStateFromError` оновлює стан для запасного UI (синхронно, під час рендеру). `componentDidCatch` потрібен для логування (після commit-фази). - Boundaries перехоплюють лише помилки фази рендерингу в нащадках. Обробники подій та async-код потребують `try/catch`. - Обгортай будь-який піддеревець, що може падати незалежно: сторонні віджети, панелі дашборду, lazy-маршрути. ### Швидкий приклад ```jsx 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. Очікування перехоплення помилок з обробників подій** ```jsx // НЕ перехоплюється - обробники подій виконуються поза render-циклом React <ErrorBoundary> <button onClick={() => { throw new Error('boom'); }}>Натисни</button> </ErrorBoundary> ``` Використовуй `try/catch` безпосередньо всередині обробника. **2. Пропустити `getDerivedStateFromError` і покластися тільки на `componentDidCatch`** ```jsx // Неправильно - setState тут ставить у чергу ще один рендер-цикл, fallback з'являється із затримкою componentDidCatch(error, info) { this.setState({ hasError: true }); } ``` Додай `static getDerivedStateFromError`, щоб fallback рендерився в тому ж проходженні що й помилка. **3. Спроба написати boundary як функціональний компонент** ```jsx // Не працює 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-рівні. ## Приклади ### Базовий: перехоплення помилки рендерингу ```jsx 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 ```jsx 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 не перехоплюють ```jsx // 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()` безпосередньо на місці виклику.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.