useEffect Hook

f

useEffect — это не «жизненный цикл», а синхронизация

Одна из самых частых ошибок, которую я вижу у разработчиков с опытом до двух лет, — попытка мыслить в парадигме классовых компонентов. useEffect — это не замена componentDidMount, componentDidUpdate и componentWillUnmount. Это механизм синхронизации вашего React-компонента с внешним миром (API, DOM, таймеры, подписки).

Когда вы пишете useEffect(() => { document.title = `Привет, ${name}`; }, [name]), вы говорите React: «Каждый раз, когда значение name меняется, сделай так, чтобы заголовок документа соответствовал этому значению». Не «сделай это при монтировании» и не «сделай это при обновлении». Это ментальный сдвиг, который сразу убирает половину потенциальных багов.

Типичная ловушка: забытая очистка (cleanup)

Представьте: вы подписываетесь на событие resize в эффекте и забываете вернуть функцию очистки. В одностраничном приложении пользователь переходит на другую страницу — компонент размонтируется, но обработчик продолжает висеть в памяти. При повторном монтировании навешивается второй, затем третий — и через несколько переходов приложение тормозит.

Зависимости: чем точнее, тем стабильнее

Массив зависимостей — самая частая причина трудновоспроизводимых багов. Пропуск зависимости приводит к тому, что эффект использует «старое» значение переменной (замыкание). Это называется stale closure. В 2026 году линтер eslint-plugin-react-hooks уже стандарт де-факто, но многие его отключают или игнорируют.

Профессиональный совет: никогда не подавляйте предупреждения линтера комментарием // eslint-disable-next-line без крайней необходимости. Если линтер ругается — значит, вы либо пропустили зависимость, либо используете нестабильную ссылку (например, объект или функцию, созданную на лету). В последнем случае помогает useCallback или useRef для хранения стабильной ссылки.

Асинхронные операции внутри useEffect

Нельзя передать асинхронную функцию напрямую в useEffect. Вот так писать нельзя:

useEffect(async () => { await fetchData(); }, []); — это вернёт промис, а useEffect ожидает функцию, возвращающую void или очистку. На практике такой код вызовет ошибку или неочевидное поведение.

  1. Правильный паттерн — объявить асинхронную функцию внутри эффекта и сразу её вызвать: (async () => { ... })().
  2. Обрабатывайте race conditions. Если компонент дважды быстро перерендерится, вы можете получить ответ от старого запроса после нового. Используйте флаг let ignore = false; — проверяйте его перед установкой стейта.
  3. Используйте AbortController — передавайте сигнал в fetch, и при размонтировании или смене зависимостей отменяйте предыдущий запрос.
  4. Не злоупотребляйте useEffect для запросов. Для сложной работы с данными рассмотрите React Query, SWR или RTK Query — они уже содержат оптимизации.
  5. Кэширование ответов. Если данные не меняются часами, нет смысла запрашивать их при каждом монтировании. Храните кэш в sessionStorage или в состоянии выше по дереву.
  6. Обработка ошибок. Всегда ловите ошибки в асинхронной функции, иначе необработанный промис «упадёт» молча (в 2026 году Node.js и браузеры уже пишут warning, но лучше не рисковать).
  7. Загрузка и индикация. Устанавливайте стейт загрузки до вызова асинхронной функции, а не после — иначе на долю секунды мелькнёт предыдущее состояние.

Когда useEffect не нужен: альтернативные подходы

Многие разработчики злоупотребляют useEffect для вещей, которые React умеет делать нативно. Это ведёт к лишним рендерам и сложному коду. Проверьте себя: если ваш эффект просто копирует значение из пропсов в локальный стейт — скорее всего, он лишний. Используйте прямой расчёт (derived state) или useMemo.

useLayoutEffect: когда промедление смерти подобно

Мало кто знает, что есть близнец useEffect — useLayoutEffect. Он запускается синхронно после всех мутаций DOM, но до того, как браузер успел отрисовать кадр. Используйте его, когда вам нужно:

Опасность: useLayoutEffect блокирует отрисовку, поэтому не кладите в него тяжёлые вычисления или асинхронные операции. Если можно обойтись обычным useEffect — используйте его.

Практический пример: исправляем баг с бесконечным ререндером

Представьте: компонент получает объект filters и при его изменении делает запрос. Разработчик написал: useEffect(() => { fetchData(filters); }, [filters]). Проблема: если filters создаётся заново при каждом рендере (например, { status: 'active', sort: 'asc' }), эффект будет срабатывать каждый раз — даже если значения не изменились. Бесконечный ререндер и спам API-запросами гарантирован.

Решение: передавайте пропс filters либо как стабильную ссылку (через useMemo в родителе), либо используйте стейт внутри компонента и меняйте его только по реальному изменению. Либо используйте глубокое сравнение (хотя встроенного нет — придётся писать свой хук или использовать JSON.stringify, но это костыль).

Ещё один трюк: если вам нужен эффект без зависимостей (только при монтировании), убедитесь, что вы случайно не добавляете туда функцию или объект, созданный на месте. Выносите их за пределы компонента или используйте useRef.

Запомните: useEffect — мощный, но коварный инструмент. Чем меньше эффектов в компоненте, тем проще его поддерживать. Стремитесь к тому, чтобы каждый эффект делал ровно одну вещь и имел ясную очистку.

Добавлено: 23.04.2026