Кожен рендер компонента - окремий виклик функції зі своїми значеннями props і стану. Функції, створені під час рендеру (обробники, колбеки ефектів), «запам'ятовують» значення цього рендеру. Якщо функція живе довше за рендер, вона бачить старі значення - це застаріле замикання (stale closure).
Класичний приклад - інтервал:
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count завжди 0 - замикання першого рендеру
}, 1000);
return () => clearInterval(id);
}, []); // ефект створено один раз
return <p>{count}</p>; // зупиняється на 1
}
Способи виправити:
1. Функціональне оновлення - не читати стан у замиканні взагалі:
setCount((c) => c + 1);
Найкращий варіант, коли нове значення залежить лише від попереднього.
2. Додати значення в залежності - ефект перезапуститься з новим замиканням:
useEffect(() => { /* ... */ }, [count]);
Коректно, але для інтервалу означає перестворення таймера щосекунди.
3. useEffectEvent - логіка, що має бачити свіжі значення, але не перезапускати ефект:
const onTick = useEffectEvent(() => setCount(count + step));
useEffect(() => {
const id = setInterval(onTick, 1000);
return () => clearInterval(id);
}, []);
4. useRef з поточним значенням - старий спосіб (ref оновлюється на кожному рендері, колбек читає ref.current). Працює, але useEffectEvent робить це явніше.
Де ще трапляється:
- обробники сторонніх бібліотек, зареєстровані один раз (карта, WebSocket);
useCallbackз неповними залежностями - мемоізована функція бачить старий стан;- асинхронні функції: після
awaitзначення змінних - ті, що були на момент виклику, навіть якщо стан уже змінився.
Профілактика: правило лінтера react-hooks/exhaustive-deps знаходить більшість застарілих замикань у ефектах і мемоізації. Попередження «missing dependency» - майже завжди справжня помилка, а не надмірна прискіпливість лінтера.