Что будет, если в useEffect не указать массив зависимостей?
Если в useEffect не указать массив зависимостей (т.е. не передать второй аргумент), то эффект будет выполняться после каждого рендера компонента, независимо от того, изменились ли какие-либо значения. Это поведение может вызывать ненужные повторные вызовы, проблемы с производительностью и даже бесконечные циклы перерендеривания при определённых условиях.
Синтаксис useEffect без массива зависимостей
useEffect(() => {
// Этот код выполнится после КАЖДОГО рендера компонента
});
Если не указывать второй аргумент ([]), React считает, что эффект зависит от всего, что есть в компоненте, и, не зная точно, от чего именно, вызывает его после каждого рендера.
Поведение useEffect без зависимостей
Когда useEffect вызывается без массива зависимостей:
-
Он срабатывает после первого рендера (маунта).
-
Он срабатывает после каждого обновления (re-render).
-
Он вызывает функцию очистки (если она есть) перед следующим запуском эффекта и перед размонтированием компонента.
Пример:
function MyComponent() {
const \[count, setCount\] = useState(0);
useEffect(() => {
console.log('Effect сработал');
return () => {
console.log('Очистка эффекта');
};
});
return <button onClick={() => setCount(count + 1)}>Click</button>;
}
Каждое нажатие на кнопку увеличивает count, компонент перерисовывается, и useEffect:
-
сначала вызывает console.log('Очистка эффекта')
-
затем снова console.log('Effect сработал')
Потенциальные проблемы
1. Плохая производительность
Если внутри useEffect выполняется тяжёлая операция (запрос, расчёт, подписка и т.д.), она будет выполняться на каждом рендере, даже если её запуск не требуется:
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(console.log);
});
Такой код будет отправлять запрос при каждом рендере, что может:
-
создать лишнюю нагрузку на сервер,
-
засорить логи,
-
привести к зависаниям интерфейса,
-
вызвать превышение лимита API.
2. Бесконечный цикл рендеров
Если внутри useEffect происходит обновление состояния, это вызовет новый рендер, который снова вызовет эффект, который снова обновит состояние:
function App() {
const \[count, setCount\] = useState(0);
useEffect(() => {
setCount(count + 1); // 🔁 бесконечный цикл
});
return <div>{count}</div>;
}
Цикл render → useEffect → setCount → render → useEffect → ... не остановится. Это поведение разрушает компонент и может зависеть от браузера или лимитов React.
3. Мутирующее поведение и подписки
Если useEffect подписывается на внешний источник (например, WebSocket, DOM-событие, таймер), и нет массива зависимостей, то:
-
подписка создаётся при каждом рендере,
-
не всегда корректно очищается,
-
в результате может возникать множество подписок одновременно.
Пример:
useEffect(() => {
const handler = () => console.log('scroll');
window.addEventListener('scroll', handler);
});
При каждом рендере создаётся новая подписка — и все продолжают работать, даже старые. В результате — накопление мусора и дублирование вызовов.
Сравнение разных вариантов useEffect
Сигнатура | Поведение эффекта |
---|---|
useEffect(() => {}) | После каждого рендера |
--- | --- |
useEffect(() => {}, []) | Только один раз после маунта |
--- | --- |
useEffect(() => {}, [value]) | При маунте и каждом изменении value |
--- | --- |
useEffect(() => { return () => {} }) | Очистка перед каждым следующим вызовом |
--- | --- |
Почему React требует массив зависимостей?
Второй аргумент в useEffect позволяет React оптимизировать поведение:
-
React сравнивает значения из массива зависимостей между рендерами.
-
Эффект перезапускается только если хотя бы одна зависимость изменилась.
-
Это позволяет экономить ресурсы и выполнять побочные действия только при необходимости.
Без массива зависимостей React просто не может определить, нужно ли повторно запускать эффект — поэтому он запускается всегда.
Когда осознанно можно опустить зависимости?
В реальной разработке — почти никогда. Но есть редкие случаи:
-
Когда вы хотите, чтобы эффект срабатывал всегда после любого рендера, как аналог componentDidUpdate.
-
Когда зависимость меняется настолько часто, что проще запускать эффект всегда.
-
Когда зависимости не имеют значения (например, логирование рендеров в dev-режиме).
Но даже в этих случаях предпочтительно использовать ESLint-директиву:
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
// особый код
});
Как отлавливать такие ошибки
Подключите ESLint с правилом react-hooks/exhaustive-deps. Оно:
-
предупреждает, если вы забыли передать зависимости,
-
рекомендует правильный список зависимостей,
-
помогает избежать неожиданных багов.
Пример правильного использования useEffect
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData);
return () => controller.abort();
}, \[\]); // Запускаем запрос только при маунте компонента
Этот эффект выполнится только один раз — при первом монтировании компонента, и будет безопасно очищен при размонтировании (через abort()), что предотвращает утечки памяти и гонки данных.