useEffect: 15 сложных вопросов с разбором (что реально спрашивают на интервью)

Глубокий разбор useEffect для junior/middle: timing после commit, зависимости, stale closure, cleanup, StrictMode в React 18/19, race condition и типичные ошибки на интервью.

01 марта 2026 г.22 минLexicon Team

useEffect — самый «опасный» хук в React не потому, что он сложный по API, а потому что его слишком легко применить не по назначению. В простых кейсах все выглядит безобидно, но в реальных проектах именно вокруг эффектов чаще всего возникают бесконечные циклы, гонки запросов и трудноуловимые баги со старыми значениями.

На middle-собеседованиях тему useEffect почти всегда копают глубоко. Интервьюер хочет понять, различаете ли вы render и commit, умеете ли работать с зависимостями, понимаете ли cleanup и можете ли объяснить поведение StrictMode в React 18/19.

Поверхностное понимание звучит так: «эффект запускается после рендера». Реальное понимание — это причинно-следственная модель: когда запускается, почему перезапускается, что сравнивается в dependencies, какие риски даёт замыкание и как безопасно синхронизироваться с внешним миром.

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

Если хотите сначала освежить базу по ререндерам и мемоизации, держите рядом React rendering: когда компонент перерисовывается и React.memo, useMemo и useCallback: оптимизация без магии. Для целостной картины фаз mount/update/unmount полезно также прочитать Жизненный цикл компонента в React в 2026 году.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью.

Подписаться

Коротко: что такое useEffect на самом деле

useEffect — это инструмент синхронизации React-компонента с внешним миром:

  • сеть;
  • подписки;
  • таймеры;
  • DOM API и сторонние библиотеки;
  • любое состояние вне «чистой» рендер-логики.

Он не предназначен для «любой логики подряд». Если задачу можно решить в render, обработчике события или через вычисляемое значение, часто useEffect вообще не нужен.

Ключевой момент по таймингу: эффект запускается после commit-фазы, то есть после применения изменений к DOM.

Короткая схема:

  1. Render
  2. Commit
  3. Запуск эффекта
  4. Cleanup (если нужен)

Отсюда и важное отличие от render-фазы: render должен оставаться чистым и предсказуемым, а useEffect работает с побочными эффектами уже после обновления UI.

15 сложных вопросов с разбором

1. Когда именно выполняется useEffect?

useEffect выполняется после commit-фазы:

  • после того, как React применил изменения в DOM;
  • асинхронно относительно render-фазы;
  • в очереди эффектов текущего коммита.

Частая ошибка кандидатов: путать это с componentDidMount и считать, что useEffect — просто «современный lifecycle». На практике у него другая модель: один API для mount/update/unmount-синхронизации.

2. Почему useEffect вызывается дважды в StrictMode?

В React 18+ в dev-режиме StrictMode имитирует последовательность mount -> unmount -> mount для проверки устойчивости эффектов.

Зачем это нужно:

  • выявить побочные эффекты, которые не очищаются;
  • поймать ошибки cleanup-логики;
  • обнаружить неидемпотентные сценарии.

Важно: это поведение dev-only. В production двойного запуска из-за StrictMode нет, но игнорировать его нельзя, потому что он показывает реальные баги, которые потом проявляются в других условиях.

3. Что произойдёт, если не указать dependency array?

Эффект будет выполняться после каждого рендера компонента.

Это не всегда ошибка, но это высокий риск:

  • лишние запросы;
  • лишние подписки;
  • циклы через setState;
  • деградация производительности.

Если dependencies отсутствуют, у вас должна быть осознанная причина, почему эффект должен срабатывать каждый раз.

4. Почему возникает бесконечный цикл?

Классический пример:

useEffect(() => {
  setCount(count + 1);
}, [count]);

Разбор:

  1. Эффект срабатывает.
  2. Вызывает setState.
  3. Компонент ререндерится.
  4. count изменился, эффект срабатывает снова.

И так по кругу.

Как исправляют:

  • пересматривают цель эффекта;
  • убирают лишний setState;
  • используют условие выхода;
  • переносят вычисление в render/derived value, если это не побочный эффект.

5. Почему React требует указывать все зависимости?

Потому что эффект замыкает значения из текущего рендера. Если dependency не указан, React не понимает, когда эффект должен быть пересоздан с актуальными данными.

Правило exhaustive-deps защищает от тихих багов:

  • stale closures;
  • рассинхронизация с актуальным состоянием;
  • «случайно работающий» код, который ломается позже.

На интервью сильный ответ: dependencies — это не формальность для линтера, а декларация того, от каких значений зависит корректность эффекта.

6. Что такое stale closure?

Stale closure — ситуация, когда эффект «видит» старую версию переменной из момента, когда функция эффекта была создана.

Почему это происходит:

  • JavaScript-замыкание фиксирует значения текущего рендера;
  • dependency array не обновляет эффект, если нужное значение не указано.

Как исправляют:

  • корректно указывают зависимости;
  • используют функциональные обновления setState(prev => ...), где это уместно;
  • выносят volatile-значения в ref только при обоснованной необходимости.

7. Когда cleanup вызывается?

Cleanup-функция вызывается:

  • перед повторным запуском эффекта (если dependencies изменились);
  • при размонтировании компонента;
  • в dev StrictMode — в рамках проверочного цикла mount/unmount/mount.

Идея проста: если эффект что-то «подключил», cleanup должен это «отключить». Подписки, таймеры, внешние слушатели и abort-запросы должны закрываться явно.

8. Почему нельзя делать async напрямую в useEffect?

Неверный паттерн:

useEffect(async () => {
  // ...
});

Проблема: useEffect ожидает, что callback вернет void или cleanup-функцию. async всегда возвращает Promise, а это не cleanup.

Правильный подход:

useEffect(() => {
  let cancelled = false;

  async function run() {
    const data = await loadData();
    if (!cancelled) {
      setData(data);
    }
  }

  run();

  return () => {
    cancelled = true;
  };
}, []);

9. Как отменить fetch в useEffect?

Стандартный способ — AbortController + cleanup:

useEffect(() => {
  const controller = new AbortController();

  fetch("/api/items", { signal: controller.signal })
    .then((res) => res.json())
    .then(setItems)
    .catch((err) => {
      if (err.name !== "AbortError") throw err;
    });

  return () => controller.abort();
}, []);

Что это дает:

  • не обновляете state после размонтирования;
  • снижаете риск race condition;
  • не держите лишние висящие запросы.

10. В чём разница между useEffect и useLayoutEffect?

Разница в тайминге:

  • useEffect — после paint (не блокирует отрисовку);
  • useLayoutEffect — синхронно после commit, но до paint.

Когда нужен useLayoutEffect:

  • чтение/измерение layout перед показом кадра;
  • синхронная корректировка DOM, чтобы избежать визуального скачка.

Во всех остальных случаях лучше useEffect, потому что layout-эффекты могут блокировать отрисовку и ухудшать UX.

11. Когда useEffect вообще не нужен?

Очень важный вопрос для middle.

useEffect обычно не нужен, когда задача решается:

  • как вычисляемое значение в render (const fullName = ...);
  • как derived state без отдельного хранения;
  • как реакция на действие пользователя в event handler.

Типичный антипример:

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

Здесь можно обойтись без эффекта:

const fullName = `${firstName} ${lastName}`;

Сильный ответ на интервью: если нет синхронизации с внешним миром, сначала ищем решение без useEffect.

12. Как избежать гонки запросов (race condition)?

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

Решения:

  • AbortController для отмены неактуального запроса;
  • guard-флаг (ignore/cancelled) в cleanup;
  • серверная дедупликация или ключи запросов в data-layer.

Суть: компонент должен применять только актуальный результат для текущего состояния UI.

13. Почему setState внутри эффекта не всегда проблема?

Иногда это нормально и ожидаемо.

Например:

  • вы получили данные извне и сохраняете их;
  • синхронизируете состояние с внешним источником;
  • реагируете на подписку/событие.

Проблема не в самом setState, а в неконтролируемом цикле и неочевидных зависимостях. Если есть понятная причина, корректный dependency array и cleanup, setState внутри эффекта — рабочий паттерн.

14. Как работает dependency comparison?

React сравнивает зависимости по Object.is:

  • для примитивов — сравнение значений;
  • для объектов/массивов/функций — сравнение ссылок.

Почему inline-объекты ломают логику:

useEffect(() => {
  // ...
}, [{ page, limit }]); // новая ссылка на каждом рендере

Даже если page и limit те же, объект новый, значит эффект перезапустится.

Решения:

  • декомпозировать зависимости на примитивы;
  • стабилизировать объект через useMemo, если это действительно нужно;
  • пересмотреть архитектуру эффекта.

15. Чем useEffect отличается от событийного обработчика?

Коротко:

  • useEffect — синхронизация состояния компонента с внешним миром после commit;
  • event handler — реакция на конкретное действие пользователя (click, submit, input).

Где должна жить бизнес-логика:

  • логика, привязанная к действию пользователя, — в обработчике;
  • логика синхронизации с внешней системой — в эффекте.

Когда это смешивают, код становится неявным: действие пользователя запускает цепочки через effect, которые сложнее отследить и тестировать.

Частые ошибки кандидатов

  • Используют useEffect для вычислений, а не для синхронизации.
  • Игнорируют зависимости или подавляют линтер без объяснения.
  • Не понимают stale closure и получают «старые» данные.
  • Пишут async напрямую в callback эффекта.
  • Отключают ESLint правилом «чтобы не ругался», не решая причину.

На интервью это быстро считывается как отсутствие системного понимания, даже если синтаксис хука кандидат знает.

Как правильно отвечать на интервью

Формула сильного ответа:

  1. Объяснить timing: эффект запускается после commit.
  2. Показать понимание dependencies и сравнения по ссылке.
  3. Объяснить, когда и зачем нужен cleanup.
  4. Упомянуть StrictMode double invoke в dev.
  5. Привести пример race condition и способ защиты.

Если ответ строится по этой схеме, он звучит как инженерное рассуждение, а не как повторение документации.

Отработай useEffect-вопросы в формате интервью

Практика на реальных сценариях: зависимости, cleanup, гонки запросов и аргументация уровня middle.

Начать подготовку

Мини-чеклист для middle

Вы действительно понимаете useEffect, если можете:

  • объяснить stale closure на примере;
  • объяснить двойной вызов в StrictMode;
  • написать отменяемый fetch;
  • избежать бесконечного цикла в эффекте;
  • аргументированно сказать, когда useEffect не нужен.

FAQ

useEffect заменяет lifecycle?

Частично по сценариям, но не как прямое отображение методов классов. Это единый API для синхронизации после commit и очистки ресурсов.

Нужно ли всегда писать dependency array?

Не всегда. Но если массив не указан, эффект запускается после каждого рендера. Это должно быть осознанным решением, а не случайностью.

Можно ли использовать несколько useEffect?

Да, и обычно так лучше. Разделение эффектов по ответственности делает код понятнее и снижает риск связных багов.

useEffect замедляет приложение?

Сам по себе не обязательно. Замедляет неправильное использование: лишние перезапуски, тяжелая работа в эффекте и отсутствие контроля зависимостей.

Итоги

useEffect — это инструмент синхронизации, а не универсальный хук для любой логики.

Большая доля сложных багов в React-приложениях действительно связана с неправильным применением useEffect: зависимостями, cleanup и гонками запросов.

На middle уровне проверяют не знание синтаксиса, а понимание механики: timing, зависимости, жизненный цикл эффекта и способность объяснять практические компромиссы.

Автор

Lexicon Team

Читайте также