> Как правильно указывать зависимости в useEffect в React (React)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: Иннотех, Точка, EvApps, EPAM, intelsy

Стек: React

> Пример ответа

Короткий ответ

Правильное указание зависимостей в useEffect - это включение всех реактивных значений (props, state, переменных из тела компонента), которые используются внутри эффекта. Пропуск зависимостей приводит к stale closures и багам. Используйте линтер eslint-plugin-react-hooks с правилом exhaustive-deps для автоматической проверки. Если эффект должен выполниться только при монтировании, передавайте пустой массив, но только если внутри нет реактивных значений.

Подробное объяснение

useEffect в React принимает callback и массив зависимостей. React сравнивает предыдущие зависимости с текущими через Object.is и перезапускает эффект при изменении любой из них. Основные правила:

  1. Все реактивные значения должны быть в массиве: state, props, переменные, функции или объекты, объявленные внутри компонента. Это включает значения, полученные через useState, useReducer, useMemo, useCallback, а также пропсы.

  2. Исключения: refs (через useRef) и dispatch от useReducer стабильны и не требуются в зависимостях. Функции, объявленные вне компонента или переданные через props, тоже не реактивны.

  3. Пустой массив []: эффект выполняется только при монтировании и cleanup при размонтировании. Используйте только если внутри нет реактивных значений - иначе получите stale closure, где эффект "запоминает" старые значения.

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

  5. Правильная работа с функциями: если эффект использует функцию, объявленную внутри компонента, её нужно либо включить в зависимости, либо обернуть в useCallback со стабильными зависимостями.

На практике

  • Всегда запускайте линтер с правилом exhaustive-deps. Он подскажет, какие зависимости пропущены.
  • Если линтер предлагает добавить зависимость, но вы считаете это ненужным - скорее всего, вы ошибаетесь. Пересмотрите логику.
  • Для асинхронных операций внутри эффекта используйте паттерн с флагом отмены, чтобы избежать обновления размонтированного компонента.
  • Если эффект зависит от props, которые меняются часто, подумайте о debounce или throttle.
  • Избегайте включения в зависимости объектов или массивов, создаваемых при каждом рендере - это вызовет бесконечный цикл. Используйте useMemo или примитивные значения.

Пример кода

JSX
// Плохо: пропущена зависимость userId
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // userId не указан - stale closure
return <div>{user?.name}</div>;
}
// Хорошо: все зависимости указаны
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
fetchUser(userId).then(data => {
if (!cancelled) setUser(data);
});
return () => { cancelled = true; };
}, [userId]); // userId в зависимостях
return <div>{user?.name}</div>;
}
// Сложный случай: функция в зависимостях
function SearchResults({ query, onResult }) {
useEffect(() => {
search(query).then(onResult);
}, [query, onResult]); // onResult может меняться каждый рендер
}
// Решение: стабилизируем onResult через useCallback
function SearchResults({ query, onResult }) {
const stableOnResult = useCallback(onResult, []); // если onResult стабилен
useEffect(() => {
search(query).then(stableOnResult);
}, [query, stableOnResult]);
}

Как отвечать на собеседовании

Начните с базового правила: все реактивные значения в зависимостях. Приведите пример stale closure с пропущенной зависимостью. Объясните, почему пустой массив - это не "только при монтировании", а "без зависимостей". Упомяните линтер как обязательный инструмент. Для senior-уровня добавьте про паттерн отмены асинхронных операций, стабилизацию функций через useCallback и проблемы с объектами/массивами в зависимостях. Покажите понимание, когда useEffect не нужен - например, для вычислений на основе state используйте useMemo.

Что проверяет интервьюер

  • Понимание механизма сравнения зависимостей через Object.is.
  • Осознание проблемы stale closures и способов её избежать.
  • Умение работать с асинхронными операциями в эффектах (cleanup, отмена запросов).
  • Знание trade-off между разными подходами (пустой массив, без массива, с зависимостями).
  • Способность объяснить, почему линтер - это не просто рекомендация, а обязательное правило.

Типичные ошибки

  • Пропуск зависимостей "потому что они не меняются": даже если значение кажется стабильным, оно может измениться при ререндере родителя.
  • Включение ненужных зависимостей: например, dispatch или ref - они стабильны, но их включение не вредит, хотя избыточно.
  • Игнорирование линтера: добавление // eslint-disable-next-line без понимания последствий.
  • Использование пустого массива для эффекта, который читает props или state: приводит к багам, которые сложно отловить.
  • Создание новых объектов/функций при каждом рендере и передача их в зависимости: вызывает бесконечный цикл эффекта.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью