🧠 1. Overview of Stale Closure in React
In React, a common but subtle bug is the "stale closure" — which happens when a function inside a component uses an outdated value, not reflecting the current state or props.
This often leads to unexpected behavior, especially in callbacks, useEffect
, or asynchronous logic.
Understanding how JavaScript creates closures and how React handles re-renders is key to avoiding this issue.
📦 2. Recap: Scope & Closure in JavaScript
Scope defines the visibility of variables. In JavaScript, we have function scope and block scope.
A closure occurs when a function remembers variables from the scope where it was defined, even if it's executed elsewhere.
Example:
function outer() {
let count = 0;
return function inner() {
console.log(count); // closure retains the value of count
};
}
Closures are powerful, but in React, they can cause a function to "remember" stale values — and that’s the root of stale closures.
⚠️ 3. Common Cases of Stale Closure
a. Callback inside useEffect
or useCallback
const [text, setText] = useState("");
const handleLog = useCallback(() => {
console.log(text); // text may be stale if not included in dependency array
}, []);
b. Async function in event handler
const handleSubmit = async () => {
await delay(1000);
console.log(value); // value might be outdated
};
c. Event listeners or subscriptions
useEffect(() => {
const handler = () => {
console.log(data); // data might be stale
};
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
🛠️ 4. How to Fix It
✅ Use callback to update state
When updating state based on the previous state, always use a callback:
setCount(prev => prev + 1); // avoid setCount(count + 1)
✅ Include variables correctly in dependency arrays
useEffect(() => {
// always use correct dependencies
doSomething(value);
}, [value]);
✅ Use useRef
to store the latest value
const latestValue = useRef(value);
useEffect(() => {
latestValue.current = value;
}, [value]);
const handleClick = () => {
console.log(latestValue.current);
};
✅ Use useEvent
(the latest version of React or custom hook equivalent)
If the newest version of React doesn't have it yet, you can create your own:
function useEvent(callback) {
const cbRef = useRef(callback);
useEffect(() => {
cbRef.current = callback;
});
return useCallback((...args) => cbRef.current(...args), []);
}
5. Conclusion 🎯
Stale closure is a natural consequence of how JavaScript handles closures and how React separates the render cycle from effects and callbacks.
Understanding this mechanism helps you write more stable and easier-to-debug React code.
Always ask yourself:
"At what point in time is this function using the value?" — and you will avoid many bugs.