EvolCode
треки/Веб от нуля до сайта·03 / 11
7 мин чтения

useEffect — действия извне компонента

useState — это память. useEffect — это РУКИ компонента. Когда нужно сходить за данными в API, поставить таймер, послушать события окна — это всё useEffect. Один из самых сложных хуков, новички с ним мучаются. Разберём правильно.

Зачем useEffect

Render компонента должен быть ЧИСТОЙ функцией: те же props/state → тот же UI. Никаких побочных эффектов внутри. Но иногда нужно: загрузить данные, подписаться на keyboard events, поставить таймер — это всё ПОБОЧНОЕ. useEffect выносит эти действия за рамки рендера, в специальную фазу «после того как DOM обновился».

Базовый синтаксис

$ useEffect(() => { /* эффект */ }, [зависимости])

Функция выполнится после того как React отрендерил компонент. Массив зависимостей — это «когда повторять». [] (пустой) — только один раз при монтировании. [count] — каждый раз когда count меняется. Без массива — после КАЖДОГО рендера (почти никогда не нужно).

Cleanup — функция возврата

Если эффект что-то «открывает» (таймер, подписку, запрос) — это нужно «закрыть» при размонтировании или перед следующим запуском. Возвращай из useEffect функцию:

$ return () => { clearInterval(timer) }

React вызовет её перед следующим запуском эффекта или когда компонент удалится. Без этого — утечки памяти и баги «таймеры от прошлого экрана продолжают работать».

Загрузка данных

Стандартный паттерн: useEffect с пустым массивом → fetch → setData. Учитывай race conditions: если компонент быстро перемонтировался, ответ от старого fetch может прийти после нового и затереть данные. Решение — флаг cancelled или AbortController. Альтернатива: использовать SWR / TanStack Query / Next.js Server Components — они решают это сами.

Зависимости — главная ловушка

Если эффект использует переменную из компонента — она ОБЯЗАНА быть в зависимостях. Иначе эффект «застревает» на старом значении (closure stale). React-линтер react-hooks/exhaustive-deps подсвечивает забытые. Не выключай его. Лечится либо добавлением зависимости, либо useCallback/useMemo, либо вынесением функции из компонента.

примерtsx
import { useState, useEffect } from 'react';

// === ЗАГРУЗКА ДАННЫХ ===
function UserProfile({ id }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);

    fetch(`/api/users/${id}`)
      .then(r => r.json())
      .then(data => {
        if (!cancelled) {
          setUser(data);
          setLoading(false);
        }
      });

    return () => { cancelled = true };    // cleanup от race condition
  }, [id]);                                 // повторить при смене id

  if (loading) return <p>Загрузка...</p>;
  return <p>Привет, {user.name}</p>;
}


// === ТАЙМЕР с cleanup ===
function Clock() {
  const [time, setTime] = useState(new Date());

  useEffect(() => {
    const timer = setInterval(() => {
      setTime(new Date());
    }, 1000);

    return () => clearInterval(timer);    // ОБЯЗАТЕЛЬНО — иначе таймер живёт после удаления
  }, []);

  return <p>{time.toLocaleTimeString()}</p>;
}


// === ПОДПИСКА на события ===
function KeyTracker() {
  const [key, setKey] = useState('');

  useEffect(() => {
    function onKey(e) {
      setKey(e.key);
    }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  return <p>Последняя клавиша: {key}</p>;
}


// === ОДНОРАЗОВЫЙ запуск (на mount) ===
function Analytics() {
  useEffect(() => {
    fetch('/api/track', { method: 'POST', body: JSON.stringify({ page: 'home' }) });
  }, []);   // [] = только при монтировании
  return null;
}


// === АНТИПАТТЕРН — забытые зависимости ===
function BadCounter({ initialCount }) {
  const [count, setCount] = useState(initialCount);

  // ❌ initialCount НЕ в зависимостях
  // Если родитель сменит initialCount — count останется старым
  useEffect(() => {
    setCount(initialCount);
  }, []);   // ← должно быть [initialCount]

  return <p>{count}</p>;
}


// === ПРАВИЛЬНО ===
function GoodCounter({ initialCount }) {
  const [count, setCount] = useState(initialCount);

  useEffect(() => {
    setCount(initialCount);
  }, [initialCount]);

  return <p>{count}</p>;
}


// === АЛЬТЕРНАТИВА для загрузки данных — SWR ===
// import useSWR from 'swr';
//
// function UserSWR({ id }) {
//   const { data, error } = useSWR(`/api/users/${id}`, fetcher);
//   if (error) return <p>Ошибка</p>;
//   if (!data) return <p>Загрузка</p>;
//   return <p>{data.name}</p>;
// }
// Никаких useEffect вручную — SWR делает кеш, refetch, race conditions сам.

Главные сценарии useEffect

главное
  • 01useEffect = код который выполняется ПОСЛЕ рендера; для запросов, таймеров, подписок
  • 02Массив зависимостей: [] = один раз, [a, b] = при смене a или b, без массива = каждый рендер
  • 03cleanup-функция (return) — обязательна для таймеров и подписок, иначе утечки
  • 04fetch с race-condition — ставь flag cancelled или используй AbortController
  • 05Все переменные используемые в эффекте должны быть в зависимостях — линтер ловит
  • 06Не выключай react-hooks/exhaustive-deps — он спасает от багов закрытий
  • 07Для запросов в продакшне лучше SWR / TanStack Query — они решают стандартные проблемы
проверь себя
01useEffect с массивом [] — когда срабатывает?
02Что произойдёт если забыть cleanup в useEffect с setInterval?
03Эффект использует переменную `userId` из props, но её нет в массиве зависимостей. В чём проблема?
следующий урок: Формы в React и список с фильтрацией