Why does React sometimes show stale state in closures? How do you fix it?

HighHardReact
Preparing for interviews?

Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.

Quick Answer

Explain how JavaScript closures capture values from a specific render, why React renders create new closures, and how this leads to stale state bugs in effects, timeouts, and event handlers — and how to fix them.

Answer

Short answer

Because each render creates new variables and new functions, and JavaScript closures capture the values from the render they were created in. If an async callback or effect runs later, it may still see old state from an earlier render.

The mental model

React components are not long-lived objects. They are more like:
"Functions that React calls again and again to produce UI."

Every render creates a new snapshot of props, state, and functions. Closures created in that render freeze those values.

JSX
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // ❌ Might log stale value
    }, 1000);

    return () => clearInterval(id);
  }, []); // empty deps

  return <button onClick={() => setCount(c => c + 1)}>+</button>;
}
                  

Why this logs stale state

This effect runs only once. The closure inside setInterval captures count from the first render (which is 0).

Even if state updates later, this callback still sees count = 0 forever. Not a React bug. Just JavaScript closures + React render model.

Cause

What’s actually happening

Symptom

Closure captures old render values

Callback keeps references to old variables

State appears "stuck" or outdated

Effect deps are missing

Effect never re-runs with new state

Logic uses old props/state

Async logic (timeout, interval, promise)

Runs after render that created it

Reads stale snapshot

Why stale state bugs happen

Fix #1: Add correct dependencies

Tell React to recreate the closure when state changes:

JSX
useEffect(() => {
  const id = setInterval(() => {
    console.log(count);
  }, 1000);

  return () => clearInterval(id);
}, [count]); // ✅ now closure updates when count changes
                  

Fix #2: Use functional updates (most common fix)

If you are updating state based on previous state, don’t read from the closure at all:

JSX
setCount(c => c + 1); // ✅ always uses latest state
                  

Fix #3: Use useRef for mutable latest value

If you need to read the latest value from long-lived async callbacks:

JSX
const countRef = useRef(count);
useEffect(() => {
  countRef.current = count;
}, [count]);

useEffect(() => {
  const id = setInterval(() => {
    console.log(countRef.current); // ✅ always latest
  }, 1000);
  return () => clearInterval(id);
}, []);
                  

Important insight

This is not a React quirk. This is how JavaScript closures work combined with React’s "each render is a snapshot" model.

Interview framing

Say it like this:
"Each render creates a new closure with its own state snapshot. Async callbacks keep the snapshot from the render they were created in. Stale state happens when effects or callbacks don’t re-run with new dependencies. You fix it with correct deps, functional updates, or refs."

Summary

Stale state happens because closures capture values from a specific render. React re-renders create new snapshots, but old async callbacks keep old ones. Fix it by using correct dependencies, functional state updates, or a ref for reading the latest value.

Similar questions
Guides
12 / 41