Вы пишете красивый компонент на React, всё работает идеально, но консоль упорно подсвечивает желтым предупреждение: \"React Hook useEffect has a missing dependency\". Знакомо? Эта ошибка — не просто раздражающее предупреждение, а важный механизм, защищающий вас от коварных багов, связанных с замыканиями и устаревшими значениями. Давайте разберемся, почему она возникает, как правильно её исправлять и когда можно (осторожно!) игнорировать.
Что на самом деле говорит это предупреждение?
Когда ESLint с правилом react-hooks/exhaustive-deps ругается на пропущенную зависимость, он указывает на фундаментальный принцип работы хуков: все значения из внешней области видимости, которые используются внутри эффекта, должны быть перечислены в массиве зависимостей. Если вы используете переменную, проп или состояние внутри useEffect, но не включаете её в зависимости, React не сможет перезапустить эффект при её изменении, что приведет к использованию устаревшего значения.
Это правило — не прихоть, а защита от самой частой ошибки при работе с хуками: \"stale closure\" (устаревшее замыкание). Эффект \"запоминает\" значения переменных на момент своего создания.
Типичные сценарии и решения
1. Использование пропсов или состояния внутри эффекта
Самый частый случай. Вы хотите выполнить побочный эффект при изменении конкретного пропса.
Проблемный код:
useEffect(() => {
fetchData(userId); // userId — проп или состояние
}, []); // Пустой массив, но используется userId!
Правильное решение:
useEffect(() => {
fetchData(userId);
}, [userId]); // userId добавлен в зависимости
2. Функции внутри эффекта
Если вы используете функцию, объявленную в теле компонента, её тоже нужно добавлять в зависимости.
Часто это приводит к бесконечным циклам, если функция пересоздается при каждом рендере. Решение — useCallback.
const fetchData = useCallback(async () => {
const response = await api.get(`/users/${userId}`);
setData(response.data);
}, [userId]); // Зависимости функции
useEffect(() => {
fetchData();
}, [fetchData]); // Теперь функция стабильна
3. Когда эффект должен запускаться только один раз
Иногда эффект действительно должен запуститься только при монтировании (аналог componentDidMount). Если внутри него используются переменные, которые не должны вызывать повторный запуск, у вас есть несколько вариантов:
- Переместить переменную внутрь эффекта, если это возможно.
- Использовать useRef для хранения изменяемого значения, которое не должно триггерить эффект.
- Отключить линтер для конкретной строки комментарием
// eslint-disable-next-line react-hooks/exhaustive-deps— но это крайняя мера!
Опасность игнорирования предупреждения
Почему нельзя просто везде добавлять // eslint-disable-next-line? Рассмотрим классический пример с интервалом:
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // Всегда будет 0!
}, 1000);
return () => clearInterval(id);
}, []); // count не в зависимостях
Интервал будет всегда \"видеть\" начальное значение count = 0, потому что эффект создал замыкание на момент первого рендера. Добавление [count] в зависимости исправило бы баг, но перезапускало бы интервал каждую секунду. Правильное решение — функциональное обновление состояния или useRef.
Практическая стратегия работы с зависимостями
- Сначала добавляйте все зависимости, которые требует линтер.
- Анализируйте, не приводит ли это к бесконечным циклам или ненужным ререндерам.
- Оптимизируйте: используйте useCallback для функций, useMemo для значений, выносите логику из компонента.
- Только в крайнем случае отключайте правило, написав поясняющий комментарий, почему это безопасно.
Помните: пустой массив зависимостей [] — это осознанное утверждение \"этот эффект действительно не зависит ни от каких значений из компонента\". Проверяйте это утверждение дважды.
FAQ: Частые вопросы об ошибке зависимостей useEffect
Можно ли полностью отключить это правило?
Технически — да, в конфиге ESLint. Но крайне не рекомендуется. Это ваш главный защитник от трудноуловимых багов.
Почему useEffect запускается в бесконечный цикл после добавления зависимостей?
Значит, одна из зависимостей (часто это функция, объект или массив) изменяется при каждом рендере. Нужно стабилизировать её с помощью useCallback, useMemo или вынести за пределы компонента.
Что делать, если зависимость — это функция из пропсов, которую я не могу мемоизировать?
Если функция приходит из пропсов и стабильна (не пересоздается в родительском компоненте), можно безопасно добавить её в зависимости. Если она нестабильна — возможно, нужно мемоизировать её на уровне родителя или пересмотреть архитектуру.
В чём разница между [] и отсутствием второго аргумента?
useEffect(fn) (без массива) запускается при каждом рендере. useEffect(fn, []) запускается только при монтировании. useEffect(fn, [dep]) — при монтировании и изменении dep.
Как правильно работать с асинхронными функциями внутри useEffect?
Не делайте колбэк асинхронным напрямую. Вместо этого создайте асинхронную функцию внутри эффекта и сразу вызовите её, либо используйте IIFE.
useEffect(() => {
const loadData = async () => {
const result = await fetch(url);
setData(result);
};
loadData();
}, [url]);