Как работает useEffect? Зачем нужен массив зависимостей и какие с ним бывают проблемы?
Короткий ответ
- useEffect выполняет побочный эффект после коммита рендера
- Массив зависимостей определяет, когда эффект перезапускается
- Функция очистки вызывается перед повтором и при размонтировании
- Пустой массив — эффект один раз после монтирования
- Пропущенная зависимость даёт устаревшее замыкание (stale closure)
- Объект или функция в зависимостях — перезапуск каждый рендер
- Эффект — для синхронизации с внешним миром, не для вычислений
useEffect синхронизирует компонент с внешними системами после рендера, а массив зависимостей и функция очистки управляют его жизненным циклом; большинство багов — из-за нечестных зависимостей.
Как сказать вслух
пример ответаuseEffect — это способ выполнить побочный эффект после того, как React обновил страницу: подписаться на событие, запустить запрос, завести таймер. Массив зависимостей говорит, при изменении каких значений эффект нужно выполнить заново, а возвращаемая функция очистки убирает за предыдущим запуском — отписывается, сбрасывает таймер. Главные проблемы — забытые зависимости, из-за которых эффект видит устаревшие данные, и нестабильные объекты в зависимостях, из-за которых он гоняется каждый рендер.
Подробный ответ
Основной ответ
Эффект запускается после коммита: DOM уже обновлён. Перед каждым повторным запуском и при размонтировании вызывается функция очистки из предыдущего запуска — пара «подписался/отписался» живёт вместе. Зависимости сравниваются по Object.is: примитивы работают предсказуемо, а объект или функция, создаваемые заново на каждом рендере, делают зависимости «всегда новыми» — эффект зацикливается; лечится useMemo/useCallback либо переносом значения внутрь эффекта. Обратная ошибка — пропустить зависимость: эффект замыкает старые значения и работает с устаревшим состоянием. Правило exhaustive-deps ловит это автоматически, и подавлять его — почти всегда ошибка. Для запросов данных в эффекте важна защита от гонок: AbortController или флаг отмены в очистке. Отдельно стоит сказать, что эффект — не место для вычислений из пропсов: это делается прямо в рендере или через useMemo.
Ключевые моменты
- Очистка. Cleanup вызывается перед каждым повторным запуском, а не только при размонтировании — это основа отписок без утечек.
- Stale closure. Пропущенная зависимость означает, что эффект навсегда запомнил значения того рендера, в котором запустился.
- Нестабильные зависимости. Новые объекты и функции на каждом рендере заставляют эффект срабатывать постоянно; нужны мемоизация или реструктуризация.
- Гонки запросов. Ответ старого запроса может прийти позже нового; отмена в cleanup через AbortController решает проблему.
Практический контекст
В реальных проектах на useEffect приходится заметная доля багов: двойные подписки, утечки таймеров, перетирание свежих данных старым ответом. В StrictMode React намеренно монтирует-размонтирует эффект дважды в dev-режиме, чтобы проявить отсутствие очистки — про это часто спрашивают. Сильный кандидат добавляет, что загрузку данных в больших приложениях выносят в библиотеки вроде TanStack Query, а эффекты оставляют для настоящей синхронизации с внешними системами.
Пример кода
useEffect(() => {
const ctrl = new AbortController();
fetch(`/api/users/${id}`, { signal: ctrl.signal })
.then(r => r.json())
.then(setUser)
.catch(e => { if (e.name !== 'AbortError') setError(e); });
return () => ctrl.abort(); // отмена при смене id или размонтировании
}, [id]);Частые ошибки
- Отключают eslint-правило exhaustive-deps вместо исправления причины
- Не пишут cleanup для подписок и таймеров, получая утечки и двойные обработчики
- Используют эффект для вычисления производного состояния, которое можно посчитать в рендере