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.
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 valueFix 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.