Navigate
Search topics, hooks, and fundamentals
A stale closure happens when a function remembers an old value that has since changed. This is the #1 source of bugs with React hooks.
When you create a function (like a timeout callback), it captures the current value of variables at that moment. If the value changes later, the old function still sees the old value — it's reading from an old time capsule:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// This closure captured count = 0 on first render
setTimeout(() => {
// Still sees count as 0, even if you clicked many times!
console.log("Count is:", count);
}, 3000);
};
return <button onClick={handleClick}>Count: {count}</button>;
}Click the increment button several times, then watch the delayed log. The stale version reads the value captured at click time. The fixed version reads the latest value via a ref:
A useRef gives you a mutable container that persists across renders. By writing the latest value to ref.current on every render, your closure always has access to the latest value:
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count; // always up to date
const handleClick = () => {
setTimeout(() => {
// Reads the ref — always gets latest value ✅
console.log("Count is:", countRef.current);
}, 3000);
};
return <button onClick={handleClick}>Count: {count}</button>;
}setTimeout, setInterval, event listeners, and effect callbacksuseRef to always read the latest valuesetCount(c => c + 1)