Skip to main content

State as a Snapshot

State looks like a normal variable you read and write. It behaves differently. Setting state doesn't change the variable you already have in hand. It triggers a re-render, and the value you're holding stays fixed until that next render. Once this clicks, a whole class of "why didn't my state update?" confusions disappears.

Setting state triggers renders

Updating the screen after an event always goes through state. When you call a setter, you're not directly editing the DOM, you're asking React to re-render with the new value (setting state requests a re-render).

Form.jsx
import { useState } from "react";

export default function Form() {
const [isSent, setIsSent] = useState(false);
const [message, setMessage] = useState("Hi!");

if (isSent) {
return <h1>Your message is on its way!</h1>;
}

return (
<form onSubmit={(e) => {
e.preventDefault();
setIsSent(true);
sendMessage(message);
}}>
<textarea
placeholder="Message"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button type="submit">Send</button>
</form>
);
}

Pressing the button does three things in order. The onSubmit handler runs, setIsSent(true) changes isSent and queues a new render, then React re-renders with the new isSent and shows the "on its way" message.

Rendering takes a snapshot in time

Here's the mental model that makes state make sense. "Rendering" means React calls your component function, and the JSX it returns is a snapshot of the UI frozen at that moment. The props, the event handlers, and every local variable inside were all calculated using the state values as they were during that render.

State doesn't live inside your function like a normal variable that vanishes when the function returns. It lives inside React, as if on a shelf outside your component. When React calls your component, it hands you a snapshot of the state for that one render. Your function then returns a UI snapshot with a fresh set of props and handlers, all built from those particular state values.

Every time React re-renders, it calls your function again, your function returns a new snapshot, and React updates the screen to match. The important consequence: each render's handlers were built with that render's state, and they keep those values forever, even after the state has moved on.

Why "+3" only adds 1

This is the example that proves the point. This button calls setNumber(number + 1) three times, yet each click only moves the counter by one.

Counter.jsx
import { useState } from "react";

export default function Counter() {
const [number, setNumber] = useState(0);

return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
);
}

The reason is that setting state only changes it for the next render. During the first render number is 0. Inside that render's click handler, number stays 0 the entire time, even after you call setNumber. So all three lines are really doing the same thing.

The way to read it is to substitute the value in. For the current render where number is 0, the handler is actually:

what-really-runs.jsx
<button onClick={() => {
setNumber(0 + 1); // React: set number to 1 next render
setNumber(0 + 1); // React: set number to 1 next render
setNumber(0 + 1); // React: set number to 1 next render
}}>+3</button>

Three requests, all "make number 1 next time." So the next render shows 1, not 3. Click again and, on that render, number is 1, so it becomes 2, and so on.

State over time

Because the value is fixed for the render, even asynchronous code sees the old value. First, a synchronous version: this alerts 0, not 5.

alert-now.jsx
<button onClick={() => {
setNumber(number + 5);
alert(number); // still 0 in this render
}}>+5</button>

setNumber(number + 5) queues the change for the next render, but number in this handler is still 0, so the alert shows 0.

Now the surprising one. Even with a 3-second delay, it still alerts 0, not 5.

alert-later.jsx
<button onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number); // still 0, three seconds later
}, 3000);
}}>+5</button>

By the time the alert fires, the state stored in React has already changed and the screen shows 5. But the alert was scheduled with the snapshot from the render where it was created, and in that render number was 0. Substituted in, it's literally:

substituted.jsx
setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);

This is the key rule: a state variable's value never changes within a render, even if the handler's code is asynchronous. The value was fixed the moment React took the snapshot by calling your component.

The same thing protects you in real scenarios. Imagine a form that sends a message after a 5-second delay:

DelayedForm.jsx
export default function Form() {
const [to, setTo] = useState("Alice");
const [message, setMessage] = useState("Hello");

function handleSubmit(e) {
e.preventDefault();
setTimeout(() => {
alert(`You said ${message} to ${to}`);
}, 5000);
}
// ...select for `to`, textarea for `message`, submit button...
}

If you press Send with "Hello" to Alice, then quickly switch the recipient to Bob before the 5 seconds are up, the alert still says "You said Hello to Alice." The handler captured to and message from the render where you clicked, so it doesn't matter that the state changed afterward. React keeps each render's values fixed inside that render's handlers, so you never have to worry that state shifted mid-execution.

(If you actually want to read the newest state before the next render, that's what a state updater function is for, which is the next page.)

Recap

Setting state requests a new render rather than changing your current variable. React stores state outside your component, and each time you call useState it hands back a snapshot of the state for that render. Variables and event handlers don't survive into the next render, every render gets its own. So every render, and every function created inside it, always sees the state snapshot React gave to that render. A handy trick is to mentally substitute the state value into the handler to predict what it does, and to remember that a handler created in a past render still holds the state values from that render.