Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «async помилки в useEffect». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)Асинхронний код всередині `useEffect` обгортають в IIFE з `try/catch/finally`, скасовують запити через `AbortController` і використовують прапорець (`cancelled` чи id запиту), щоб не викликати `setState` після анмаунту чи зміни залежностей. **Ключове:** ігноруй помилку `AbortError` (це очікуване скасування), а решту нормалізуй до `Error` і виводь у стан `error`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення## 1) Базовий патерн: IIFE + try/catch/finally ```javascript useEffect(() => { let cancelled = false; (async () => { try { setLoading(true); const res = await fetch("/api/items"); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); if (!cancelled) setItems(data); } catch (e) { // тут логуємо і виставляємо помилку в стейт const err = e instanceof Error ? e : new Error(String(e)); if (!cancelled) setError(err); // відправка в моніторинг (Sentry/GlitchTip) // reportError(err) } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [/* deps */]); ``` **Навіщо прапорець** `cancelled`**?** Щоб не викликати `setState` після анмаунту/зміни запиту. --- ## 2) Скасовуємо запити коректно: `AbortController` ```javascript useEffect(() => { const controller = new AbortController(); (async () => { try { setLoading(true); const res = await fetch(`/api/items`, { signal: controller.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setItems(data); } catch (e) { // ігноруємо скасування if (e instanceof DOMException && e.name === "AbortError") return; const err = e instanceof Error ? e : new Error(String(e)); setError(err); // reportError(err) } finally { setLoading(false); } })(); return () => controller.abort(); }, [/* deps */]); ``` **Плюси:** відсутність гонок при зміні залежностей, безпечне скасування в React 18 (Strict Mode викликає ефекти двічі в dev). --- ## 3) Валідуйте відповідь і явно кидайте помилки ```javascript const res = await fetch(url, { signal }); if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); const json = await res.json(); if (!Array.isArray(json)) throw new Error("Unexpected payload shape"); ``` Інакше "тихі" помилки перетворяться на непередбачувані баги. --- ## 4) Ретраї з експоненційною паузою (без нескінченних циклів) ```javascript async function withRetry<T>(fn: () => Promise<T>, attempts = 3) { let lastErr: unknown; for (let i = 0; i < attempts; i++) { try { return await fn(); } catch (e) { lastErr = e; await new Promise(r => setTimeout(r, 2 ** i * 300)); // 300ms, 600ms, 1200ms... } } throw lastErr; } useEffect(() => { const controller = new AbortController(); (async () => { try { setLoading(true); const data = await withRetry( () => fetch(url, { signal: controller.signal }).then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }), 3 ); setData(data); } catch (e) { if (e instanceof DOMException && e.name === "AbortError") return; setError(e as Error); } finally { setLoading(false); } })(); return () => controller.abort(); }, [url]); ``` --- ## 5) Стежте за залежностями ефекту - Усе, що використовується всередині ефекту і змінюється з рендерами, повинне бути в deps: URL, токени, фільтри тощо. - Якщо передаєте колбеки/об'єкти, **мемоізуйте** (`useCallback`, `useMemo`), щоб не смикати ефект зайвий раз. --- ## 6) Гонки запитів: "актуалізуй тільки останній" Якщо неможливо використати `AbortController` (не всі клієнти вміють), зберігайте "id запиту": ```javascript useEffect(() => { let requestId = Symbol(); const current = requestId; (async () => { try { const data = await api.load(params); if (current === requestId) setData(data); } catch (e) { if (current === requestId) setError(e as Error); } })(); return () => { requestId = Symbol(); }; }, [params]); ``` --- ## 7) Де показувати помилку - Локальний `error`-стейт -> показати inline-алерт/кнопку Retry. - Глобальні критичні - репортити в Sentry/GlitchTip. - **Error Boundary** для рендер-помилок UI; **не** для async - їх ловимо тут же, в ефекті. --- ## 8) Практичні дрібниці - У TypeScript оголошуйте `catch (e: unknown)` і нормалізуйте до `Error`. - У `finally` завжди вимикайте `loading`. - Для повторних завантажень тримайте `reload()` (змінюйте ключ або інкрементуйте `version` у deps). - Обнуляйте попередню помилку перед новою спробою (`setError(null)`), щоб UI не "залипав". --- ## 9) Коли краще не писати все вручну Для даних ідеально підходять **TanStack Query / RTK Query**: - автоматичний кеш, ретраї, скасування, дедуплікація запитів, статус `isLoading/error/data`. - Менше коду в ефектах, менше шансів помилитися.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.