Back to the blog
React· Jul 30, 2026· 7 min read

Why useState doesn't update immediately (and what to do instead)

You call setState, log the value on the next line, and it's stale. That's not a bug - it's how React works. Here's the mental model, plus the patterns for reacting to state changes correctly.

#react#usestate#hooks#state-management

Every React developer writes this code once: call setCount(count + 1), console.log(count) on the next line, and see the old value. It looks broken. It isn't - and understanding why unlocks half of React's mental model.

State is a snapshot, not a variable

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

function handleClick() {
  setCount(count + 1); // schedules a re-render with 1
  console.log(count);  // still 0 - THIS render's snapshot
  setCount(count + 1); // ❌ still 0 + 1 = 1, not 2!
}

setState doesn't mutate the variable - it schedules a re-render, and the new value only exists in that next render. Inside the current function, `count` is a constant captured when the render started. That's also why calling setCount(count + 1) twice increments once: both calls read the same snapshot.

Fix 1: functional updates for sequential changes

setCount((c) => c + 1);
setCount((c) => c + 1); // ✅ now it's 2 - each updater gets the latest value

Fix 2: don't wait for state you already have

The most common real-world version of this question is 'how do I use the new state right after setting it?' - usually to fire a request. The answer: you already have the value. You just computed it. Use the local variable instead of round-tripping through state.

function handleSelect(id: string) {
  setSelectedId(id);
  fetchDetails(id);      // ✅ use the value directly
  // not: fetchDetails(selectedId) - that's the OLD one
}

Fix 3: useEffect when the update comes from elsewhere

When you genuinely need to react to a state change you don't control locally - a value set by a child, a store, or a URL param - an effect with that value in its dependency array is the correct tool. Reserve it for that case; an effect that just mirrors a handler you own is extra indirection.

Bonus: React 18+ batches everything

Since React 18, multiple setState calls in the same tick - including inside promises, timeouts, and native event handlers - are batched into one re-render. That's a performance win, but it means you can never rely on a render happening 'between' two updates. Design handlers around snapshots and functional updates and batching becomes invisible.

Setting state is a request for the next render, not an edit to the current one. Once that clicks, this entire class of bug disappears.

Written by Appesto Engineering.