State not updating

2 snippets in React

REReact

Stale Closures

RE · Common Mistakes
Syntax
// PROBLEM: handler captures old state value
// FIX: use updater function or ref
Example
function BrokenCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      // BUG: count is always 0 (captured from initial render)
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []); // empty deps = closure captures initial count

  return <p>{count}</p>; // stuck at 1
}

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

  useEffect(() => {
    const id = setInterval(() => {
      // FIX: updater function always has the latest value
      setCount(prev => prev + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <p>{count}</p>; // increments correctly
}

Note Stale closures happen when a callback captures a state value that never updates. Common in setInterval, setTimeout, and event listeners set up in useEffect. Use the updater form of setState or store values in a ref.

Direct State Mutation

RE · Common Mistakes
Syntax
// BAD: mutating state directly
state.items.push(newItem);
setState(state);

// GOOD: create a new reference
setState({ ...state, items: [...state.items, newItem] });
Example
function BrokenTags() {
  const [tags, setTags] = useState(['react', 'hooks']);

  function addTag(tag) {
    // BUG: push mutates the existing array
    tags.push(tag);
    setTags(tags); // same reference, React sees no change
  }

  return (
    <div>
      {tags.map(t => <span key={t}>{t}</span>)}
      <button onClick={() => addTag('new')}>Add</button>
    </div>
  );
}

function FixedTags() {
  const [tags, setTags] = useState(['react', 'hooks']);

  function addTag(tag) {
    // CORRECT: spread creates a new array
    setTags(prev => [...prev, tag]);
  }

  return (
    <div>
      {tags.map(t => <span key={t}>{t}</span>)}
      <button onClick={() => addTag('new')}>Add</button>
    </div>
  );
}

Note React uses reference equality (Object.is) to decide whether to re-render. Mutating an object or array in place keeps the same reference, so React skips the update. Always produce a new object/array via spread, map, filter, or slice.

Frequently asked questions

How does React handle state not updating?
React covers this with 2 copy-ready snippets on this page. The "Stale Closures" snippet in React uses `// PROBLEM: handler captures old state value`.
Which code does the React example use?
The "Stale Closures" snippet uses `// PROBLEM: handler captures old state value`, from the Common Mistakes section of the React cheat sheet.
What other React snippets are shown for "state not updating"?
Besides "Stale Closures", this page also shows "Direct State Mutation".
Is there anything to watch out for?
Yes. For "Stale Closures": Stale closures happen when a callback captures a state value that never updates. Common in setInterval, setTimeout, and event listeners set up in useEffect. Use the updater form of setState or store values in a ref.