Topic 4.4
State as a Snapshot
In one line
Each render sees a fixed snapshot of state: the values never change while that render's code (including its event handlers and timers) runs, and calling a setter only asks React for a new render with new values.
Think of it like this
A photo of the scoreboard. You take a photo at half-time: it says 1–0. The match goes on and the real score becomes 3–2, but your photo still says 1–0, forever. Each render of a component is a photo like that. Its state values are frozen into it. Setting state doesn't edit the photo; it asks React to take a new photo.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Snapshot
- A frozen picture: the state values one render saw, which never change during that render.
- Render
- One call of your component function by React, producing the UI for that moment.
- Closure
- A function that remembers the variables of the place where it was created.
- Schedule a render
- Ask React to render again soon (after your handler finishes), instead of right now.
- Stale value
- An old value a function still sees after the state has moved on.
- Updater function
- A function passed to a setter, like
setN(n => n + 1), that gets the latest queued value (Topic 4.5).
Step by step
01Render #1 sees count = 0
React calls Counter. useState returns 0, so in this whole call count is the constant 0. The button's handler is created inside this call.
function Counter() {
const [count, setCount] = useState(0); // render #1: count is 0
function handleClick() {
setCount(count + 5);
console.log(count);
}
return <button onClick={handleClick}>{count}</button>;
}02Substitute the snapshot
To predict what a handler does, replace each state variable with its value in that render. Render #1's handler is really:
setCount(0 + 5);
console.log(0); // prints 0, not 503React renders again with the new value
After the handler finishes, React calls Counter again. This time useState returns 5, and a brand-new handleClick is created that sees 5.
04A timer keeps its render's snapshot
A setTimeout started in render #1's handler still sees 0 when it fires, even though the screen shows 5 by then.
function handleClick() {
setCount(count + 5);
setTimeout(() => {
console.log(count); // 0: the value when you clicked
}, 3000);
}05Need the new value now? Compute it first
Calculate the next value into a local variable, use it, and pass it to the setter.
function handleClick() {
const next = count + 5;
setCount(next);
console.log(next); // 5
}Try it yourself
- 1
Predict the timer
In the first example the code already clicked once, so the button shows 5. Click it yourself three times quickly. Predict what the three timers print. (5, 10 and 15: each timer prints the value from the render its click belonged to, never the newest one.)
- 2
Log the new value correctly
In the first example, compute
const next = number + 5;first, passnextto the setter and lognext. The second line now prints 5. - 3
Switch before sending
In the messenger, swap the two
setTimeouttimes at the bottom (switch at 100, send at 200). Predict who gets the message.
Code & diagrams
The handler and its timer were created in the render where `number` was 0, so both print 0, even though React has already rendered 5 by the time the timer fires.
Preview
Console
Nothing logged yet.Expected console output
render: number = 0
right after setNumber: 0
render: number = 5
timer, 300 ms later, sees: 0The message goes to Alice, because she was selected when Send was clicked. Switching to Bob during the delay changed the next render, not the old one. This is the behaviour you want for a real Send button.
Preview
Console
Nothing logged yet.Expected console output
Sending in 500 ms...
Sent "Hello" to Alice
The screen now says: To: BobEach render creates a new `handleClick` with its own frozen `walk`. Calling the old handler from render 1 later still sees `true`, even though the screen shows Stop.
Preview
Console
Nothing logged yet.Expected console output
this handler's snapshot: walk = true
renders so far: 2
this handler's snapshot: walk = trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Read state right after setting it
In a form, write setName(input); if (name === "") console.log("Name is required"); to validate what was just typed.
Break #2
An interval that never moves
Start setInterval(() => setSeconds(seconds + 1), 1000) once, from the first render's click handler.
Myth vs fact
Myth
State updates are asynchronous, which is why the old value appears.
Fact
Even if React re-rendered instantly, your handler's count would still be old: it's a const in a finished render's closure. Snapshots, not timing, explain it.
Myth
A timer should always see the latest state.
Fact
A timer sees the snapshot of the render that created it. For most actions (send, save, confirm) that is exactly what you want. Use refs or updaters when you need the latest value.
Myth
Setting state re-renders immediately, in the middle of your handler.
Fact
React waits until the handler finishes and then re-renders once for all the updates (Topic 4.5).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The snapshot model is what makes concurrent rendering (React 18) safe: React may prepare a render in the background or throw it away, and no running code ever sees state change under its feet.
- ▸
Stale closures are the root of most "my effect sees an old value" bugs. The fixes, in order of preference: an updater function, moving the logic into the event handler,
useEffectEvent(React 19.2), or a ref holding the latest value (Phases 7 and 8). - ▸
flushSyncfromreact-domforces React to render and commit synchronously inside a handler (useful before measuring the DOM or scrolling). Even then, the handler's owncountvariable stays old; only the DOM is updated.
Remember this
- 1
Rendering means React calls your component function. The JSX it returns is a picture of the UI at that moment, calculated with that render's props and state. Every variable in that call, including
count, is a normal JavaScript constant for the whole call. - 2
A render's state never changes. When you write
const [count, setCount] = useState(0),countis just a number React gave this particular render.setCount(5)can't reach into the running function and change aconst; it stores5in React and schedules another render. In that next render,useStatereturns5into a newcountvariable. - 3
Event handlers belong to a render. The
handleClickcreated during render #1 sees render #1'scount. So aftersetCount(count + 5), aconsole.log(count)on the next line still prints the old value. The new value appears only in the next render. This is ordinary JavaScript: a function closes over the variables of the call where it was created (a closure; see the /javascript course on closures). - 4
Timers keep the old snapshot too. If a handler starts
setTimeout(() => console.log(count), 3000), the timer callback was made in that render, so after 3 seconds it prints the value from when you clicked, even if the screen has moved on since. React's docs show this withalert(number); here we use the console. This is usually what you want: a "Send" button that waits a few seconds should send the message to the person who was selected when you clicked, not whoever is selected later. - 5
Setting state asks for a new render; it doesn't change anything immediately. React re-renders after your event handler finishes. That's why you can't "set then read" state in the same handler. If you need the new value right away, compute it into a local variable first:
const next = count + 5; setCount(next); console.log(next);. When an update must be based on the latest queued value instead of the snapshot, use an updater function (Topic 4.5). - 6
Sometimes you do need the latest value inside a long-lived callback (an interval, a subscription). Then the snapshot gets in the way. The tools for that are refs (
useRef, Phase 8) anduseEffectEvent(Phase 7). For ordinary clicks, the snapshot behaviour is a feature: it makes each render predictable.
Explain it without notes
What does "state is a snapshot" mean?
Why does console.log(count) right after setCount(count + 1) print the old value?
A Send button waits 3 seconds before sending. The user changes the recipient during the wait. Who gets the message, and why?
How do you use the new value in the same handler where you set it?
Practice
Predict exactly what this logs when you click once, then explain:
``jsx
const [n, setN] = useState(10);
function handleClick() {
setN(n * 2);
console.log(n);
setTimeout(() => console.log(n), 100);
}
Write a DelayedLike button that waits 1 second and then logs Liked post <id> for the post that was open when it was clicked, using a postId state with two buttons to switch posts.
Trade-offs
- ↔
Snapshots make each render predictable and easy to reason about, at the cost of stale values in long-lived callbacks; for those, reach for updaters, refs or effect events instead of fighting the model.
Done when you can
I can explain why state looks unchanged right after calling its setter.
I can predict what a timer started in a handler will print.
I can substitute snapshot values into a handler to predict its behaviour.
I can compute the next value into a local variable when I need it immediately.