Chapter 23: State as a Snapshot
Core Idea
A state variable's value inside any given render (and every closure created during that render, including event handlers) is fixed — calling the setter doesn't mutate that value in place, it only schedules a future render with a new snapshot.
Key Concepts
- Render = calling your function and getting a JSX snapshot. Props, local variables, and event handler closures in that snapshot were all computed from the state values at that render, and stay fixed for the lifetime of that render — they don't retroactively change when the setter is called.
- State lives outside the function, conceptually "on a shelf" in React itself. What your component receives on each call is a snapshot of that outside value for that specific render — not a live reference to it.
- The classic surprise: calling a setter three times in a row inside one click handler with the same computed next value (e.g.
setNumber(number + 1) three times) does not increment three times — because within that render's handler, number is the same fixed value all three times, so all three calls request the identical next value. The state ends up +1, not +3, after that click.
- Mental substitution trick: to reason about a handler, substitute the state variable with its literal value for that render everywhere it appears in the closure — this makes it obvious why repeated calls with the same expression don't compound within one render.
- Setting state only changes it for the next render — never for the render currently executing, no matter how many times or how "late" in the handler you call the setter.
Code Examples
const [number, setNumber] = useState(0);
<button onClick={() => {
setNumber(number + 1); // requests 0 + 1 = 1
setNumber(number + 1); // still requests 0 + 1 = 1 (number hasn't changed in this render)
setNumber(number + 1); // still requests 0 + 1 = 1
}}>+3</button>
- What it demonstrates: three setter calls using the same render's fixed
number value all request the same next value — the count ends up 1, not 3, after one click.
Key Takeaways
- Never expect a state variable to reflect a setter call you just made earlier in the same event handler — it won't, by design.
- To accumulate multiple updates within one handler, you need the updater-function form (
setNumber(n => n + 1)), which is the subject of the next chapter — plain setNumber(number + 1) repeated N times only ever computes the same "current + 1."
- Debugging "state seems stale inside my handler" is almost always this exact snapshot behavior, not a bug.
Connects To
- Ch 24 (Queueing a Series of State Updates): the updater-function fix for exactly this limitation.
- Ch 22 (Render and Commit): the render step that produces each snapshot.