useEffect — действия извне компонента
useState — это память. useEffect — это РУКИ компонента. Когда нужно сходить за данными в API, поставить таймер, послушать события окна — это всё useEffect. Один из самых сложных хуков, новички с ним мучаются. Разберём правильно.
Зачем useEffect
Render компонента должен быть ЧИСТОЙ функцией: те же props/state → тот же UI. Никаких побочных эффектов внутри. Но иногда нужно: загрузить данные, подписаться на keyboard events, поставить таймер — это всё ПОБОЧНОЕ. useEffect выносит эти действия за рамки рендера, в специальную фазу «после того как DOM обновился».
Базовый синтаксис
Функция выполнится после того как React отрендерил компонент. Массив зависимостей — это «когда повторять». [] (пустой) — только один раз при монтировании. [count] — каждый раз когда count меняется. Без массива — после КАЖДОГО рендера (почти никогда не нужно).
Cleanup — функция возврата
Если эффект что-то «открывает» (таймер, подписку, запрос) — это нужно «закрыть» при размонтировании или перед следующим запуском. Возвращай из useEffect функцию:
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, либо вынесением функции из компонента.
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