🧠 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.
Top comments (0)