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

Хук useEffect в React позволяет выполнять побочные эффекты в функциональных компонентах. Побочные эффекты — это операции, которые не связаны напрямую с рендерингом интерфейса, но часто необходимы в приложении: получение данных с сервера, изменение DOM вручную, настройка подписок, таймеров, логирование и т.д. До появления хуков такие операции реализовывались в методах жизненного цикла классовых компонентов (componentDidMount, componentDidUpdate, componentWillUnmount). useEffect объединяет их функциональность в едином API.

1. Синтаксис useEffect

useEffect(() => {
// побочный эффект
return () => {
// очистка эффекта (опционально)
};
}, \[dep1, dep2\]);
  • Первый аргумент — функция, внутри которой выполняется эффект.

  • Второй аргумент — массив зависимостей. React будет повторно вызывать эффект только тогда, когда изменится хотя бы одна из зависимостей.

2. Когда вызывается useEffect

Эффект вызывается после того, как компонент отрендерен и отрисован в DOM. Он не блокирует рендеринг и выполняется асинхронно по отношению к нему. Это позволяет избежать задержек в интерфейсе.

3. Типы зависимостей и поведения

a. Без второго аргумента (выполняется после каждого рендера):

useEffect(() => {
console.log('Эффект каждый раз');
});

Вызывается после каждого рендера — как при маунте, так и при обновлениях. Используется редко, так как может приводить к избыточным вызовам.

b. Пустой массив зависимостей (выполняется один раз при маунте):

useEffect(() => {
console.log('Монтирование компонента');
}, \[\]);

Эффект будет выполнен один раз, при монтировании компонента. Это аналог componentDidMount.

c. Список зависимостей (выполняется при изменении зависимостей):

useEffect(() => {
console.log('Обновилось значение name');
}, \[name\]);

Эффект будет срабатывать только если значение name изменилось с предыдущего рендера.

4. Очистка эффекта (return-функция)

Функция, возвращаемая из useEffect, вызывается перед:

  • размонтированием компонента (аналог componentWillUnmount)

  • повторным выполнением эффекта (если изменились зависимости)

Пример с подпиской:

useEffect(() => {
const id = setInterval(() => {
console.log('Таймер');
}, 1000);
return () => {
clearInterval(id);
console.log('Таймер остановлен');
};
}, \[\]);

Очистка необходима для предотвращения утечек памяти и «висячих» подписок.

5. Пример с запросом данных

import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const \[user, setUser\] = useState(null);
useEffect(() => {
fetch(\`/api/users/${userId}\`)
.then(res => res.json())
.then(data => setUser(data));
}, \[userId\]);
return <div>{user ? user.name : 'Загрузка...'}</div>;
}
  • Эффект срабатывает при первом рендере и каждый раз, когда меняется userId.

  • Если не указать зависимости, fetch будет вызываться при каждом рендере, что неэффективно.

6. Общие случаи использования useEffect

  • Получение данных (fetch, axios)

  • Подписка на WebSocket, события, потоки

  • Манипуляции с DOM (например, установка фокуса)

  • Таймеры и интервалы (setTimeout, setInterval)

  • Интеграция с внешними библиотеками (Google Maps, D3 и т.п.)

  • Сохранение данных в localStorage или сессии

  • Отслеживание изменений props/state

7. Что может быть в зависимостях

Массив зависимостей может содержать:

  • props (например, userId, isOpen)

  • значения из useState

  • функции, полученные извне

  • значения из useCallback, useMemo

Важно: все зависимости должны быть перечислены. React не проверяет их автоматически — если вы не укажете какую-то переменную, это приведёт к багам (например, эффект будет использовать устаревшее значение).

8. Использование с асинхронными функциями

useEffect не принимает async-функцию напрямую, потому что она возвращает промис, а React ожидает функцию очистки или undefined.

Правильный способ:

useEffect(() => {
async function fetchData() {
const res = await fetch('/api/data');
const data = await res.json();
setData(data);
}
fetchData();
}, \[\]);

9. Потенциальные ошибки при использовании useEffect

  • Отсутствие зависимостей — эффект не обновляется при изменении данных.

  • Лишние зависимости — эффект вызывается слишком часто.

  • Неправильная очистка — висячие обработчики событий, утечки памяти.

  • Асинхронные гонки — запросы могут приходить в неправильном порядке.

Для борьбы с гонками можно использовать флаг isMounted или AbortController:

useEffect(() => {
let isCancelled = false;
async function loadUser() {
const res = await fetch('/api/user');
if (!isCancelled) {
setUser(await res.json());
}
}
loadUser();
return () => {
isCancelled = true;
};
}, \[\]);

10. Хуки внутри эффектов

Внутри useEffect нельзя вызывать хуки (useState, useEffect, useContext и т.п.) — это нарушает правила хуков. Все хуки должны быть на верхнем уровне тела компонента.

11. Зависимость от функции

Если в useEffect используется функция, созданная внутри компонента, она будет создаваться заново при каждом рендере. Это приведёт к перезапуску эффекта. Чтобы избежать этого, можно использовать useCallback:

const handleClick = useCallback(() => {
console.log('Клик!');
}, \[\]);
useEffect(() => {
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, \[handleClick\]);

12. Поведение при ререндере

Если зависимость не изменилась (сравнивается поверхностно через Object.is), React не перезапускает эффект. Это даёт возможность контролировать точные условия, при которых эффект будет вызван.

13. useEffect vs useLayoutEffect

  • useEffect выполняется после отрисовки DOM.

  • useLayoutEffect — до отрисовки, синхронно. Это важно при прямой манипуляции DOM, когда нужно измерить элементы до того, как пользователь увидит их.

В 99% случаев используют useEffect, потому что useLayoutEffect может блокировать интерфейс.