Gernar
Frontend DeveloperReact

Интервью-вопрос

Что если передать массив в useEffect

Массив во втором аргументе useEffect задает, когда эффект нужно повторить. На интервью вам важно не только назвать [], [deps] и отсутствие массива. Еще стоит объяснить stale closure, сравнение по ссылке и cleanup.

Добавлен
Редакция

Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.

🐰0
🥚0

Мини-квиз

Проверка перед разбором

Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.

Вопрос 1 из 50 правильно

Что лучше ответить про пустой массив зависимостей?

Вы объясняете поведение useEffect(() => { ... }, []).

Варианты ответа

Разбор

Разобраться, а не зазубрить

Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.

Базовая идея

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

Короткий ответ на интервью можно построить так:

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

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

Три основных варианта

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

// 1. После каждого рендера
useEffect(() => {
  console.log("render happened");
});

// 2. После первого коммита, затем cleanup при размонтировании
useEffect(() => {
  console.log("mounted");
}, []);

// 3. После первого коммита и при изменении userId
useEffect(() => {
  console.log("user changed", userId);
}, [userId]);

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

Как React понимает, что зависимость изменилась

React сравнивает элементы массива зависимостей с предыдущими значениями через Object.is. Для примитивов это обычно ожидаемое поведение. Для объектов, массивов и функций важна ссылка.

Проблемный пример:

function Users({ status }) {
  const filters = { status };

  useEffect(() => {
    fetchUsers(filters);
  }, [filters]);
}

Здесь filters создается заново на каждом рендере. Даже если status не изменился, ссылка новая. Значит, эффект может запускаться лишний раз.

Более безопасный вариант:

function Users({ status }) {
  useEffect(() => {
    const filters = { status };
    fetchUsers(filters);
  }, [status]);
}

Теперь зависимость отражает реальную причину запроса. Если статус не меняется, эффект не запускается только из-за нового объекта.

Как выбирать зависимости

Хорошая формулировка звучит так: зависимости должны соответствовать значениям, которые эффект читает из компонента. Если эффект использует userId, token или enabled, эти значения должны быть в массиве зависимостей, если только вы не переписали код так, что они больше не нужны.

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

Как рассуждать про зависимости

1Эффект читает props, state или значение из тела компонента?
Укажите это значение в зависимостях или перепишите эффект так, чтобы он его не читал.
2Значение создается как объект, массив или функция на каждом рендере?
Вынесите создание внутрь эффекта, используйте примитивы или мемоизируйте только при реальной пользе.
3Эффект обновляет state, который сам указан в зависимостях?
Проверьте условие обновления или используйте функциональный setState, чтобы не получить цикл.
4Эффект запускает запрос, подписку или таймер?
Добавьте cleanup, отмену запроса или отписку, иначе возможны утечки и устаревшие обновления UI.

Бесконечные циклы и stale closure

Бесконечный цикл часто появляется, когда эффект обновляет state, который сам заставляет эффект снова выполниться. Иногда это нужно, но тогда должно быть условие остановки.

Проблемный пример:

function Counter() {
  const [count, setCount] = useState(0);

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

  return <span>{count}</span>;
}

Эффект меняет count. Изменение count снова запускает эффект, и цикл продолжается. В браузере это может заморозить интерфейс, заспамить аналитику или отправить серию одинаковых запросов, если внутри эффекта есть сеть. Если вам нужно один раз вычислить начальное значение, лучше сделать это в инициализаторе useState или добавить понятное условие.

Обратная ошибка это stale closure. Она возникает, когда эффект использует значение, но вы не указали его в зависимостях. Код может долго выглядеть рабочим. Потом он начнет отправлять запрос со старым id или использовать старый callback.

Асинхронные эффекты и cleanup

Для запросов важен не только массив зависимостей. Нужна еще отмена устаревшей работы. Если userId меняется быстро, старый запрос может завершиться позже нового и перезаписать актуальные данные.

function UserCard({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

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

    async function loadUser() {
      setIsLoading(true);
      setError(null);

      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal,
        });

        if (!response.ok) {
          throw new Error("Failed to load user");
        }

        const data = await response.json();
        setUser(data);
      } catch (error) {
        if (error.name !== "AbortError") {
          setError(error);
        }
      } finally {
        if (!controller.signal.aborted) {
          setIsLoading(false);
        }
      }
    }

    loadUser();

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

  if (isLoading) return <p>Loading user...</p>;
  if (error) return <p role="alert">Could not load user.</p>;

  return <pre>{JSON.stringify(user, null, 2)}</pre>;
}

Здесь [userId] говорит, когда нужно загрузить другого пользователя. Cleanup отменяет предыдущий запрос перед новым запуском эффекта и при размонтировании компонента. Проверка response.ok, loading и error state не дают показывать старые данные как успешный ответ. role="alert" помогает озвучить ошибку пользователям скринридеров.

Как это лучше сказать на интервью

Сильный ответ не должен быть длинным. Достаточно назвать поведение и сразу показать, что вы понимаете риски.

В useEffect массив во втором аргументе это список зависимостей. С пустым массивом эффект не будет повторяться из-за ререндеров, с массивом значений он повторится при изменении любого значения, без массива он запускается после каждого рендера. Я бы отдельно следил за тем, чтобы зависимости были полными, потому что иначе будет stale closure. А если эффект делает запрос, подписку или таймер, я добавлю cleanup.

Такая формулировка звучит спокойнее, чем фраза про полный аналог componentDidMount. Она показывает, что вы думаете не только о синтаксисе, но и о поведении UI.

Безопасный ход
  1. 1Назвать второй аргумент массивом зависимостей
  2. 2Объяснить пустой массив, массив с зависимостями и отсутствие массива
  3. 3Сказать про сравнение по Object.is
  4. 4Упомянуть cleanup и риск stale closure
Опасный ход
  1. 1Сказать, что пустой массив всегда ровно один запуск
  2. 2Удалить зависимость ради тишины линтера
  3. 3Положить новый объект в зависимости без причины
  4. 4Не отменить старый запрос при смене id

Частые ошибки

Где обычно ошибаются

Проверьте формулировки, которые звучат уверенно, но на интервью быстро выдают пробелы.

  1. 1

    Подбирать зависимости на глаз

    Если убрать зависимость только ради редкого запуска, эффект начнет читать старые значения из замыкания. В UI это часто выглядит как запрос не по тому id, старый обработчик, неверная аналитика или экран, который не совпадает с текущими props. Лучше указать зависимость и отдельно решить проблему частых запусков.
  2. 2

    Класть в зависимости новый объект

    Объект, массив или функция, созданные прямо в теле компонента, получают новую ссылку на каждом рендере. React посчитает зависимость изменившейся, и эффект может слать лишние запросы. Безопаснее зависеть от примитивов, создать объект внутри эффекта или мемоизировать значение осознанно.
  3. 3

    Забывать cleanup

    Подписка, таймер или запрос без очистки могут продолжить работу после размонтирования или после смены зависимости. Это дает утечки, двойные обработчики и race condition. Возвращайте функцию очистки из useEffect и отменяйте устаревшие операции.
  4. 4

    Делать сам effect async

    Функция эффекта должна вернуть cleanup или ничего, а async возвращает Promise. Из-за этого cleanup не будет описан правильно. Объявите async-функцию внутри эффекта и отдельно верните функцию очистки.
  5. 5

    Игнорировать Strict Mode в разработке

    В React Strict Mode разработческая среда может повторно запускать эффект и cleanup, чтобы найти небезопасные побочные действия. Если код отправляет необратимое действие без защиты, вы увидите дубли. На интервью лучше сказать, что эффект должен быть устойчивым к повторному запуску.

Follow-up

Что могут спросить дальше

Короткие ответы на вопросы, которыми проверяют понимание useEffect, зависимостей и жизненного цикла эффекта.

Живые ответы

Видео с похожим вопросом

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

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

Содержание