Зачем нужен эффект
Эффект синхронизирует компонент с внешней системой: подписка на WebSocket, таймер, сторонний виджет, document.title, запрос данных. Всё, что можно сделать в рендере или в обработчике события, делается там.
useEffect(() => {
const conn = createConnection(roomId);
conn.connect();
return () => conn.disconnect(); // очистка
}, [roomId]);
Думайте не «при монтировании и размонтировании», а «пока roomId такой, держи соединение с этой комнатой».
Когда запускается
- Эффект выполняется после коммита, когда браузер уже отрисовал изменения.
- Без массива зависимостей: после каждого рендера.
[]: после первого монтажа.[a, b]: после монтажа и после рендеров, гдеaилиbизменились поObject.is.
Очистка вызывается перед повторным запуском эффекта (со старыми значениями) и при размонтировании. При смене roomId с 1 на 2 порядок такой: очистка для 1, затем эффект для 2.
Эффекты детей выполняются раньше эффектов родителя.
StrictMode и двойной запуск
В режиме разработки StrictMode монтирует компонент, сразу размонтирует и монтирует снова: эффект, очистка, эффект. Так проверяется, что очистка честно отменяет то, что сделал эффект. В продакшене этого нет. Если двойной запуск что-то ломает, значит очистки не хватает, и это нужно чинить, а не отключать StrictMode.
Зависимости
Все реактивные значения (props, state, всё, что объявлено в теле компонента), которые читает эффект, должны быть в зависимостях. Линтер react-hooks/exhaustive-deps это проверяет.
Объект или функция, созданные в теле компонента, новые на каждом рендере, поэтому эффект будет перезапускаться постоянно:
const options = { roomId }; // новый объект каждый рендер
useEffect(() => { ... }, [options]); // запускается после каждого рендера
Решения: создать объект внутри эффекта, зависеть от примитивов (roomId) или мемоизировать.
Устаревшее значение
Эффект видит значения рендера, в котором создан. Таймер с [] навсегда запомнит начальный state:
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000); // count всегда 0
return () => clearInterval(id);
}, []);
// исправление: setCount(c => c + 1)
Если эффекту нужно прочитать свежее значение, но не перезапускаться из-за него, в React 19.2 есть useEffectEvent.
Загрузка данных и гонки
Ответы на запросы могут прийти не по порядку. Очистка должна «отменить» устаревший запрос:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${id}`, { signal: controller.signal })
.then(r => r.json())
.then(setUser)
.catch(e => { if (e.name !== 'AbortError') setError(e); });
return () => controller.abort();
}, [id]);
В реальных проектах загрузку данных лучше отдать фреймворку или библиотеке (TanStack Query, SWR, загрузчики роутера, серверные компоненты): там уже есть кеш, отмена и дедупликация.
Когда эффект не нужен
- Производные данные. Отфильтрованный список считайте в рендере (
useMemo, если дорого), а не кладите в state через эффект. - Реакция на действие пользователя. Отправка формы, запрос по клику, аналитика нажатия живут в обработчике.
- Сброс state при смене пропа. Используйте
key.
useLayoutEffect
Работает так же, но синхронно после изменения DOM и до отрисовки. Нужен, когда надо измерить элемент и сразу поправить позицию (тултип), чтобы не было мигания. Он блокирует отрисовку, поэтому по умолчанию используйте useEffect.
Что спросят на собесе
- Когда запускается эффект и его очистка, что означает массив зависимостей.
- Почему эффект срабатывает дважды в разработке.
- Как избежать гонки при загрузке данных.
- Чем
useLayoutEffectотличается отuseEffect. - Приведите пример, когда эффект не нужен.