Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює useEffect у React?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`useEffect`** - це хук React, який запускає побічні ефекти після рендеру компонента. ```jsx useEffect(() => { document.title = `Count: ${count}`; // Виконується після рендеру return () => { /* cleanup при розмонтуванні або перед наступним ефектом */ }; }, [count]); // Повторно запускається лише при зміні count ``` **Ключове:** порожній `[]` = один раз при монтуванні. Без deps = при кожному рендері. `[value]` = коли value змінюється.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`useEffect`** - це хук React, який запускає побічні ефекти після того, як компонент відрендерився, не блокуючи браузер від малювання екрану. ## Теорія ### TL;DR - `useEffect` виконується *після* рендеру, не під час. Спочатку з'являється UI, потім запускається ефект. - Порожній `[]` = один раз при монтуванні. Без deps = при кожному рендері. `[value]` = коли value змінюється. - Завжди повертай функцію очищення, якщо ефект створює підписку, таймер або слухач подій. - Потрібна робота після рендеру? `useEffect`. Чиста логіка стану? `useState` або reducer. - Аналогія: знак "не турбувати" в готелі - вішаєш після заселення, і прибирання (побічний ефект) приходить лише коли ти змінив знак (deps). ### Швидкий приклад ```jsx import { useState, useEffect } from 'react'; function Counter() { const [count, setCount] = useState(0); useEffect(() => { document.title = `Count: ${count}`; // Виконується після рендеру }, [count]); // Повторно запускається лише коли count змінюється return <button onClick={() => setCount(count + 1)}>Count: {count}</button>; } // Заголовок оновлюється після кожного кліку, але не на кожен рендер загалом. ``` Після рендеру React оновлює заголовок документа. Якщо `count` не змінився, ефект пропускається. Передбачувано і без сюрпризів. ### Головна відмінність від lifecycle-методів класів У класових компонентах були три окремі методи: `componentDidMount`, `componentDidUpdate` і `componentWillUnmount`. `useEffect` об'єднує їх в один хук. Повернене значення відповідає за cleanup (unmount), масив deps - за умовні повторні запуски (did update), а порожній масив - за запуск лише при монтуванні. Одна ментальна модель замість трьох. ### Коли використовувати - Завантажити дані при монтуванні: `[]` в якості deps. - Повторно завантажувати при зміні пропсу: додай цей пропс у deps `[userId]`. - Підписка або таймер: повертай функцію очищення. - Синхронізація зовнішнього стану (заголовок документа, localStorage, аналітика): включай значення, яке синхронізуєш, у deps. - Не потрібен `useEffect` для: чистих обчислень, похідного стану, синхронного читання DOM перед paint. Для останнього є `useLayoutEffect`. ### Як React планує виконання useEffect React запускає ефекти після того, як браузер намалював екран. Це відбувається під час commit-фази fiber reconciliation: React завершує мутації DOM, браузер робить paint, і тільки потім через планувальник асинхронно запускаються ефекти. Саме цим `useEffect` відрізняється від `useLayoutEffect`, який запускається синхронно після мутацій DOM, але до paint. Cleanup виконується перед наступним ефектом (якщо deps змінились) і при розмонтуванні. React відстежує ефекти через пов'язаний список хуків на fiber-вузлі, тому хуки мають викликатись в одному і тому ж порядку при кожному рендері. ### Типові помилки **Помилка 1: відсутній масив deps** ```jsx useEffect(() => { fetchData(); // Виконується при кожному рендері }); ``` Без масиву deps ефект запускається після кожного рендеру. Якщо `fetchData` оновлює стан, отримаємо нескінченний цикл. Додай `[]` або конкретні значення, від яких справді залежить ефект. **Помилка 2: немає очищення** ```jsx useEffect(() => { const timer = setInterval(tick, 1000); // Не вистачає: return () => clearInterval(timer); }, []); ``` Таймер продовжує працювати після розмонтування компонента. Це одна з найпоширеніших витоків пам'яті в React-додатках. Завжди прибирай інтервали, підписки і слухачі подій. **Помилка 3: об'єкт у масиві deps** ```jsx const config = { id: 1 }; useEffect(() => { api(config); }, [config]); // Новий об'єкт при кожному рендері ``` Об'єкти порівнюються за посиланням. `{ id: 1 } !== { id: 1 }`, тому ефект запускається при кожному рендері незалежно від значень. Рішення: залежь від примітивного значення. ```jsx useEffect(() => { api({ id: config.id }); }, [config.id]); // Стабільний примітив ``` **Помилка 4: нескінченний цикл** ```jsx function BadCounter() { const [count, setCount] = useState(0); useEffect(() => { setCount(count + 1); // count в deps викликає рендер, який знову запускає ефект }, [count]); } ``` Оновлення стану, який є у deps, = нескінченні рендери. Якщо потрібен лічильник, що сам себе інкрементує, використовуй функціональне оновлення без deps: ```jsx useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); // Без stale closure, deps не потрібні }, 1000); return () => clearInterval(id); }, []); ``` ### Де зустрічається у реальних проєктах - React Query / TanStack Query: використовує `useEffect` внутрішньо для початкових запитів перед тим, як кеш бере управління. - Next.js: `useEffect` + `useRouter` для редіректів після гідратації (серверний код не знає про роутер). - Framer Motion: вимірює DOM за допомогою `useEffect` перед запуском анімацій. - Redux Toolkit: синхронізація стану з подіями `window.focus` для відстеження активності вкладки. - Паттерн з AbortController для відміни запиту: ```jsx useEffect(() => { const abort = new AbortController(); fetch(`/api/user/${userId}`, { signal: abort.signal }) .then(res => res.json()) .then(setUser); return () => abort.abort(); }, [userId]); ``` ### Follow-up питання **Q:** Коли саме виконується `useEffect` відносно рендеру? **A:** Після того, як браузер намалював екран. React завершує зміни в DOM, браузер робить paint, і тільки потім асинхронно запускаються ефекти. Користувач бачить оновлений UI ще до того, як будь-який ефект виконається. **Q:** В чому різниця між порожнім `[]` і відсутністю deps? **A:** Порожній `[]` запускає ефект один раз після першого рендеру. Без deps - після кожного рендеру. Це принципово різна поведінка, і їх плутанина призводить до багів. **Q:** Як скасувати запит при розмонтуванні компонента? **A:** Використовуй `AbortController`. Створи його всередині ефекту, передай його signal у fetch, а в cleanup виклич `abort.abort()`. Це запобігає оновленню стану після розмонтування. **Q:** Навіщо ESLint-правило exhaustive-deps? **A:** Щоб уникнути stale closures (застарілих замикань). Якщо ефект читає змінну, але не вказує її в deps, він захоплює значення з першого рендеру і більше не оновлює його. Лінтер змушує явно вказувати залежності. **Q:** Як ефекти поводяться в concurrent mode React 18 під час transitions? **A:** Ефекти все одно виконуються після commit, навіть коли рендери переривались через transitions. React може рендерити компонент кілька разів до commit, але ефекти запускаються лише один раз за commit. Тому React 18 у Strict Mode двічі викликає ефекти в dev-режимі - щоб виявити баги в ефектах, які не є ідемпотентними. ## Приклади ### Базовий: синхронізація заголовка зі станом ```jsx import { useState, useEffect } from 'react'; function Counter() { const [count, setCount] = useState(0); useEffect(() => { document.title = `Count: ${count}`; }, [count]); return <button onClick={() => setCount(count + 1)}>Count: {count}</button>; } ``` Ефект спрацьовує один раз після монтування (встановлює заголовок "Count: 0"), а потім при кожному кліку. Якщо компонент перерендерився з іншої причини без зміни `count` - ефект пропускається. ### Середній рівень: завантаження даних з очищенням ```jsx import { useState, useEffect } from 'react'; function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { const abort = new AbortController(); fetch(`/api/user/${userId}`, { signal: abort.signal }) .then(res => res.json()) .then(setUser) .catch(err => { if (err.name !== 'AbortError') console.error(err); }); return () => abort.abort(); // Скасовуємо запит якщо userId зміниться до завершення }, [userId]); return <div>{user ? user.name : 'Loading...'}</div>; } ``` Коли `userId` змінюється, React спочатку виконує cleanup (скасовує поточний запит), а потім стартує новий. Без race condition і без оновлення стану від застарілих відповідей. ### Просунутий рівень: уникнення stale closures через useRef Бачив, як це ловить навіть досвідчених розробників. Ефект захоплює значення callback у момент свого запуску, а не визначення. ```jsx import { useState, useEffect, useRef } from 'react'; function SearchInput({ onSearch }) { const [query, setQuery] = useState(''); const onSearchRef = useRef(onSearch); // Тримаємо ref актуальним при кожному рендері, deps не потрібні useEffect(() => { onSearchRef.current = onSearch; }); useEffect(() => { if (!query) return; const timer = setTimeout(() => { onSearchRef.current(query); // Завжди використовує останній callback }, 300); return () => clearTimeout(timer); }, [query]); // Повторно запускається тільки при зміні query return <input value={query} onChange={e => setQuery(e.target.value)} />; } ``` Перший ефект (без deps) оновлює `onSearchRef.current` при кожному рендері. Другий ефект читає з ref, тому ніколи не застаріває, але повторно запускається лише при зміні `query`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.