useEffect Hook

useEffect — это не «жизненный цикл», а синхронизация
Одна из самых частых ошибок, которую я вижу у разработчиков с опытом до двух лет, — попытка мыслить в парадигме классовых компонентов. useEffect — это не замена componentDidMount, componentDidUpdate и componentWillUnmount. Это механизм синхронизации вашего React-компонента с внешним миром (API, DOM, таймеры, подписки).
Когда вы пишете useEffect(() => { document.title = `Привет, ${name}`; }, [name]), вы говорите React: «Каждый раз, когда значение name меняется, сделай так, чтобы заголовок документа соответствовал этому значению». Не «сделай это при монтировании» и не «сделай это при обновлении». Это ментальный сдвиг, который сразу убирает половину потенциальных багов.
Типичная ловушка: забытая очистка (cleanup)
Представьте: вы подписываетесь на событие resize в эффекте и забываете вернуть функцию очистки. В одностраничном приложении пользователь переходит на другую страницу — компонент размонтируется, но обработчик продолжает висеть в памяти. При повторном монтировании навешивается второй, затем третий — и через несколько переходов приложение тормозит.
- Таймеры — всегда очищайте
setIntervalиsetTimeoutчерезclearInterval/clearTimeoutв возвращаемой функции. - Подписки на WebSocket или EventSource — закрывайте соединение, иначе утечёт память и появятся лишние запросы.
- Обсерверы (IntersectionObserver, ResizeObserver) — вызывайте
.disconnect()или.unobserve(). - Запросы к API — используйте AbortController, чтобы отменить запрос, если компонент размонтировался до получения ответа. Это предотвратит попытку установить стейт на размонтированный компонент.
- Слушатели событий на window или document — всегда удаляйте через
removeEventListenerс той же функцией (не анонимной, а именованной). - CSS-анимации или transition — отменяйте их через отмену requestAnimationFrame или таймера.
Зависимости: чем точнее, тем стабильнее
Массив зависимостей — самая частая причина трудновоспроизводимых багов. Пропуск зависимости приводит к тому, что эффект использует «старое» значение переменной (замыкание). Это называется stale closure. В 2026 году линтер eslint-plugin-react-hooks уже стандарт де-факто, но многие его отключают или игнорируют.
Профессиональный совет: никогда не подавляйте предупреждения линтера комментарием // eslint-disable-next-line без крайней необходимости. Если линтер ругается — значит, вы либо пропустили зависимость, либо используете нестабильную ссылку (например, объект или функцию, созданную на лету). В последнем случае помогает useCallback или useRef для хранения стабильной ссылки.
Асинхронные операции внутри useEffect
Нельзя передать асинхронную функцию напрямую в useEffect. Вот так писать нельзя:
useEffect(async () => { await fetchData(); }, []); — это вернёт промис, а useEffect ожидает функцию, возвращающую void или очистку. На практике такой код вызовет ошибку или неочевидное поведение.
- Правильный паттерн — объявить асинхронную функцию внутри эффекта и сразу её вызвать:
(async () => { ... })(). - Обрабатывайте race conditions. Если компонент дважды быстро перерендерится, вы можете получить ответ от старого запроса после нового. Используйте флаг
let ignore = false;— проверяйте его перед установкой стейта. - Используйте AbortController — передавайте сигнал в fetch, и при размонтировании или смене зависимостей отменяйте предыдущий запрос.
- Не злоупотребляйте useEffect для запросов. Для сложной работы с данными рассмотрите React Query, SWR или RTK Query — они уже содержат оптимизации.
- Кэширование ответов. Если данные не меняются часами, нет смысла запрашивать их при каждом монтировании. Храните кэш в sessionStorage или в состоянии выше по дереву.
- Обработка ошибок. Всегда ловите ошибки в асинхронной функции, иначе необработанный промис «упадёт» молча (в 2026 году Node.js и браузеры уже пишут warning, но лучше не рисковать).
- Загрузка и индикация. Устанавливайте стейт загрузки до вызова асинхронной функции, а не после — иначе на долю секунды мелькнёт предыдущее состояние.
Когда useEffect не нужен: альтернативные подходы
Многие разработчики злоупотребляют useEffect для вещей, которые React умеет делать нативно. Это ведёт к лишним рендерам и сложному коду. Проверьте себя: если ваш эффект просто копирует значение из пропсов в локальный стейт — скорее всего, он лишний. Используйте прямой расчёт (derived state) или useMemo.
- Синхронизация нескольких стейтов. Вместо useEffect, который считает что-то при изменении A, используйте
useMemo— он чище и предсказуемее. - Обработка событий. Если действие происходит по клику, а не в ответ на изменение данных — это не задача useEffect, а обработчик onClick.
- Логирование или аналитика. Для этого useEffect подходит, но обязательно проверяйте, что отправляете данные только при реальном изменении, а не на каждый рендер.
- Вызов setState на основе предыдущего значения. Используйте функциональную форму
setState(prev => ...)внутри обработчика события, а не эффекта.
useLayoutEffect: когда промедление смерти подобно
Мало кто знает, что есть близнец useEffect — useLayoutEffect. Он запускается синхронно после всех мутаций DOM, но до того, как браузер успел отрисовать кадр. Используйте его, когда вам нужно:
- Измерить DOM-элемент (getBoundingClientRect, offsetWidth, scrollHeight) и сразу обновить стейт — без мерцания.
- Запустить анимацию с known end state, чтобы избежать визульного скачка.
- Синхронно обновить стили на основе размера окна или скролла.
- Прокрутить элемент к определённой позиции сразу после изменения данных (например, список сообщений).
Опасность: useLayoutEffect блокирует отрисовку, поэтому не кладите в него тяжёлые вычисления или асинхронные операции. Если можно обойтись обычным useEffect — используйте его.
Практический пример: исправляем баг с бесконечным ререндером
Представьте: компонент получает объект filters и при его изменении делает запрос. Разработчик написал: useEffect(() => { fetchData(filters); }, [filters]). Проблема: если filters создаётся заново при каждом рендере (например, { status: 'active', sort: 'asc' }), эффект будет срабатывать каждый раз — даже если значения не изменились. Бесконечный ререндер и спам API-запросами гарантирован.
Решение: передавайте пропс filters либо как стабильную ссылку (через useMemo в родителе), либо используйте стейт внутри компонента и меняйте его только по реальному изменению. Либо используйте глубокое сравнение (хотя встроенного нет — придётся писать свой хук или использовать JSON.stringify, но это костыль).
Ещё один трюк: если вам нужен эффект без зависимостей (только при монтировании), убедитесь, что вы случайно не добавляете туда функцию или объект, созданный на месте. Выносите их за пределы компонента или используйте useRef.
Запомните: useEffect — мощный, но коварный инструмент. Чем меньше эффектов в компоненте, тем проще его поддерживать. Стремитесь к тому, чтобы каждый эффект делал ровно одну вещь и имел ясную очистку.
Добавлено: 23.04.2026
