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.