Урок по треку React 8 мин

useEffect: синхронизация, а не жизненный цикл

Когда нужен useEffect, зависимости, очистка и типичные ошибки.

Зачем нужен эффект

Эффект синхронизирует компонент с внешней системой: подписка на 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.
  • Приведите пример, когда эффект не нужен.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.