Что будет, если в useEffect не указать массив зависимостей?

Если в useEffect не указать массив зависимостей (т.е. не передать второй аргумент), то эффект будет выполняться после каждого рендера компонента, независимо от того, изменились ли какие-либо значения. Это поведение может вызывать ненужные повторные вызовы, проблемы с производительностью и даже бесконечные циклы перерендеривания при определённых условиях.

Синтаксис useEffect без массива зависимостей

useEffect(() => {
// Этот код выполнится после КАЖДОГО рендера компонента
});

Если не указывать второй аргумент ([]), React считает, что эффект зависит от всего, что есть в компоненте, и, не зная точно, от чего именно, вызывает его после каждого рендера.

Поведение useEffect без зависимостей

Когда useEffect вызывается без массива зависимостей:

  1. Он срабатывает после первого рендера (маунта).

  2. Он срабатывает после каждого обновления (re-render).

  3. Он вызывает функцию очистки (если она есть) перед следующим запуском эффекта и перед размонтированием компонента.

Пример:

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()), что предотвращает утечки памяти и гонки данных.