Interview answer drill

Use this React interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.

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

HighIntermediateReact
Interview focus

This React interview question tests whether you can explain React stale closures: async bugs, latest-state fixes, and effect debugging, connect it to production trade-offs, and handle common follow-up questions.

  • React stale closures: async bugs, latest-state fixes, and effect debugging explanation without falling back to memorized docs wording
  • Hooks and Closure reasoning, edge cases, and production failure modes
  • How you would answer the most likely React interview follow-up
Practice more React interview questions
Interview quick answer

Explain stale closure bugs in React through timers, async handlers, subscriptions, and effects that read an old render snapshot instead of the latest state.

Full interview answer

Common production bug

Stale state in React closures is really a render-snapshot bug: each render creates new closures, and async code may keep reading an older one. That is why intervals, delayed handlers, and effects sometimes log or submit outdated values. The fix is not “React is wrong”; it is understanding when to use functional updates, refs, or correct dependencies.

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);
}, []);
                  

Worked async example: delayed submit + permission flow

Timers are only one stale-closure variant. The same bug appears when a submit handler starts async work and later reads a value that has changed since the click.

JSX
// ❌ Async callback keeps the snapshot from the click that started it
function Checkout({ canSubmit }) {
  const [status, setStatus] = useState('idle');

  async function handleSubmit() {
    setStatus('checking');
    await verifyPaymentIntent();

    if (!canSubmit) {
      setStatus('blocked');
      return;
    }

    setStatus('submitted');
  }

  return <button onClick={handleSubmit}>Pay</button>;
}

// ✅ Keep the latest permission in a ref for long-lived async work
function CheckoutFixed({ canSubmit }) {
  const latestCanSubmit = useRef(canSubmit);
  const [status, setStatus] = useState('idle');

  useEffect(() => {
    latestCanSubmit.current = canSubmit;
  }, [canSubmit]);

  async function handleSubmit() {
    setStatus('checking');
    await verifyPaymentIntent();

    if (!latestCanSubmit.current) {
      setStatus('blocked');
      return;
    }

    setStatus('submitted');
  }

  return <button onClick={handleSubmit}>Pay</button>;
}
                  

Important insight

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

When stale closures are expected

A stale closure is not automatically a bug. Sometimes you intentionally want the snapshot from the click that started the work, such as logging which filters were active when an export began. The real question is: should this callback use the latest value, or the value from the render that created it?

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
Preparing for interviews?

Use this as one explanation rep, then continue with the React interview questions cluster or a guided prep path.