useEffect
Базовое использование
Заголовок раздела «Базовое использование»import { useEffect, useState } from 'react';
// Запустить после каждого рендераuseEffect(() => { console.log('после рендера');});
// Запустить только при монтированииuseEffect(() => { console.log('только при монтировании');}, []);
// Запустить при изменении userIduseEffect(() => { fetchUser(userId);}, [userId]);Cleanup функция
Заголовок раздела «Cleanup функция»function Timer() { const [time, setTime] = useState(0);
useEffect(() => { const id = setInterval(() => setTime(t => t + 1), 1000);
// Cleanup: вызывается при размонтировании или перед следующим эффектом return () => clearInterval(id); }, []);
return <div>{time}с</div>;}
// Подписка на событияfunction WindowSize() { const [size, setSize] = useState({ w: window.innerWidth, h: window.innerHeight });
useEffect(() => { function handleResize() { setSize({ w: window.innerWidth, h: window.innerHeight }); }
window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); // cleanup! }, []);
return <div>{size.w} × {size.h}</div>;}Зависимости — правила
Заголовок раздела «Зависимости — правила»function Example({ userId, onLoad }: { userId: number; onLoad: () => void }) { const [user, setUser] = useState<User | null>(null);
// ✅ Все используемые значения — в зависимостях useEffect(() => { fetchUser(userId).then(user => { setUser(user); onLoad(); }); }, [userId, onLoad]); // userId и onLoad могут меняться
// Проблема: onLoad — новая функция при каждом рендере родителя // Решение: useCallback у родителя, или useEffectEvent (React 19)}Объекты и функции в зависимостях
Заголовок раздела «Объекты и функции в зависимостях»// ❌ Бесконечный цикл: options — новый объект каждый рендерfunction BadFetch({ userId }: { userId: number }) { const options = { headers: { Authorization: 'Bearer ...' } }; // новый объект!
useEffect(() => { fetch(`/api/users/${userId}`, options); }, [userId, options]); // options всегда "новый"}
// ✅ Вариант 1: переменные внутри эффектаfunction GoodFetch1({ userId }: { userId: number }) { useEffect(() => { const options = { headers: { Authorization: 'Bearer ...' } }; fetch(`/api/users/${userId}`, options); }, [userId]);}
// ✅ Вариант 2: useMemo / useCallback снаружиfunction GoodFetch2({ userId }: { userId: number }) { const options = useMemo( () => ({ headers: { Authorization: 'Bearer ...' } }), [] // константа );
useEffect(() => { fetch(`/api/users/${userId}`, options); }, [userId, options]);}Race conditions
Заголовок раздела «Race conditions»Проблема: несколько запросов в полёте, последний ответ может прийти не последним.
// ❌ Race condition: если userId меняется быстро, старый запрос может перезаписать новыйfunction BadUserProfile({ userId }: { userId: number }) { const [user, setUser] = useState<User | null>(null);
useEffect(() => { fetchUser(userId).then(setUser); // может записать устаревшие данные! }, [userId]);}
// ✅ Вариант 1: ignore flagfunction GoodUserProfile({ userId }: { userId: number }) { const [user, setUser] = useState<User | null>(null);
useEffect(() => { let ignore = false;
fetchUser(userId).then(data => { if (!ignore) setUser(data); // не обновляем если эффект уже "устарел" });
return () => { ignore = true; }; }, [userId]);}
// ✅ Вариант 2: AbortController (для fetch)function GoodUserProfile2({ userId }: { userId: number }) { const [user, setUser] = useState<User | null>(null);
useEffect(() => { const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal }) .then(r => r.json()) .then(setUser) .catch(err => { if (err.name !== 'AbortError') throw err; });
return () => controller.abort(); }, [userId]);}Паттерн: data fetching в useEffect
Заголовок раздела «Паттерн: data fetching в useEffect»type FetchState<T> = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error };
function useData<T>(url: string) { const [state, setState] = useState<FetchState<T>>({ status: 'idle' });
useEffect(() => { if (!url) return;
let ignore = false; setState({ status: 'loading' });
fetch(url) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json() as Promise<T>; }) .then(data => { if (!ignore) setState({ status: 'success', data }); }) .catch(error => { if (!ignore) setState({ status: 'error', error }); });
return () => { ignore = true; }; }, [url]);
return state;}
// Использованиеfunction UserCard({ userId }: { userId: number }) { const state = useData<User>(`/api/users/${userId}`);
if (state.status === 'loading') return <Spinner />; if (state.status === 'error') return <ErrorMessage error={state.error} />; if (state.status !== 'success') return null; return <div>{state.data.name}</div>;}useLayoutEffect
Заголовок раздела «useLayoutEffect»Вызывается синхронно после DOM-мутации, до paint. Используй для:
- Измерений DOM (getBoundingClientRect)
- Синхронных обновлений DOM (избежать мерцания)
function Tooltip({ children, text }: { children: React.ReactNode; text: string }) { const ref = useRef<HTMLDivElement>(null); const [pos, setPos] = useState({ top: 0, left: 0 });
useLayoutEffect(() => { if (!ref.current) return; const rect = ref.current.getBoundingClientRect(); setPos({ top: rect.bottom, left: rect.left }); }); // нет зависимостей — каждый рендер
return ( <> <div ref={ref}>{children}</div> <div style={{ position: 'fixed', ...pos }}>{text}</div> </> );}Альтернативы useEffect
Заголовок раздела «Альтернативы useEffect»// ❌ Не нужен useEffect для производных данныхfunction BadList({ items }: { items: string[] }) { const [filtered, setFiltered] = useState(items); const [query, setQuery] = useState('');
useEffect(() => { setFiltered(items.filter(i => i.includes(query))); }, [items, query]);}
// ✅ Вычисляй при рендере (или useMemo)function GoodList({ items }: { items: string[] }) { const [query, setQuery] = useState(''); const filtered = useMemo( () => items.filter(i => i.includes(query)), [items, query] );}
// ❌ Не нужен useEffect для обработки событийfunction BadForm() { const [submitted, setSubmitted] = useState(false);
useEffect(() => { if (submitted) { // навигация } }, [submitted]);}
// ✅ Обрабатывай в обработчике событияfunction GoodForm() { function handleSubmit() { // навигация сразу }}