Перейти к содержимому

useEffect

import { useEffect, useState } from 'react';
// Запустить после каждого рендера
useEffect(() => {
console.log('после рендера');
});
// Запустить только при монтировании
useEffect(() => {
console.log('только при монтировании');
}, []);
// Запустить при изменении userId
useEffect(() => {
fetchUser(userId);
}, [userId]);
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 condition: если userId меняется быстро, старый запрос может перезаписать новый
function BadUserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchUser(userId).then(setUser); // может записать устаревшие данные!
}, [userId]);
}
// ✅ Вариант 1: ignore flag
function 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]);
}
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>;
}

Вызывается синхронно после 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 для производных данных
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() {
// навигация сразу
}
}