Facebook Pixel

Closures and React: Stale Closures Explained

React hooks create closures that can capture stale state. Here is why and how to fix it.

Closures and React: Stale Closures Explained

React hooks and closures interact in a way that can cause stale closures: a callback that captures state from a previous render, not the current one. Here is why and how to fix it.

The Problem

function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); // count is from the first render (0) }, 1000); return () => clearInterval(id); }, []); // empty deps: effect runs once return <p>{count}</p>; } `` **What happens**: the `useEffect` runs once (empty deps). The `setInterval` callback closes over `count = 0` from that render. Each second, it calls `setCount(0 + 1)`, so `count` stays at 1 forever. ### Fix 1: Functional `setState` ```js useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); // uses the latest state }, 1000); return () => clearInterval(id); }, []); `` The functional form `setCount(c => c + 1)` receives the latest state as `c`. No stale closure. ### Fix 2: Add `count` to Dependencies ```js useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, [count]); // effect re-runs when count changes `` Each time `count` changes, the effect cleans up the old interval and sets up a new one with the current `count`. But this resets the timer each second, which may not be desired. ### Fix 3: `useRef` for Mutable Values ```js function Counter() { const [count, setCount] = useState(0); const countRef = useRef(count); countRef.current = count; useEffect(() => { const id = setInterval(() => { setCount(countRef.current + 1); }, 1000); return () => clearInterval(id); }, []); return <p>{count}</p>; } `` `countRef.current` always holds the latest `count`. The interval reads from the ref, not the stale closure. ### Fix 4: `useReducer` for Complex State ```js const [state, dispatch] = useReducer((state, action) => { if (action === "increment") return { count: state.count + 1 }; return state; }, { count: 0 }); useEffect(() => { const id = setInterval(() => dispatch("increment"), 1000); return () => clearInterval(id); }, []); `` `dispatch` is stable (does not change between renders), so there is no stale closure. ### Why This Happens Each React render creates new function instances with new closures. If a callback is created in one render and runs later (like a `setInterval` callback), it closes over the state from that render. If the state changes in later renders, the old callback does not know. ### The Takeaway Stale closures in React happen when a callback (in `useEffect`, `setInterval`, etc.) captures state from a previous render. Fix with functional `setState` (`setCount(c => c + 1)`), adding the state to deps, `useRef` for mutable values, or `useReducer` for complex state. The functional `setState` is usually the best fix.

A callback (in useEffect, setInterval, etc.) that captures state from a previous render, not the current one. When the state updates in later renders, the old callback still sees the old value, causing bugs.

Use the functional form of setState (setCount(c => c + 1)) which receives the latest state. Or add the state to the useEffect dependency array. Or use useRef to hold the latest value. Or use useReducer (dispatch is stable).

Because the setInterval callback closes over count from the render when useEffect ran (usually the first render, count = 0). Each tick calls setCount(0 + 1), so count stays at 1. Use setCount(c => c + 1) instead.

A ref holds a mutable .current property that persists across renders. Update countRef.current = count on each render. The interval reads countRef.current, which always has the latest value, avoiding the stale closure.

Yes. dispatch has a stable identity (it does not change between renders). So a callback that uses dispatch (not state directly) does not have a stale closure problem. This is why useReducer is a good fix for complex state in intervals.

Ready to master React completely?

Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.

Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.