Skip to content

State Snapshot & Purity

This is the single most misunderstood thing in React. When you read a state variable, you are reading its value for this render — a snapshot frozen at the moment the component was called. It does not change mid-render, even after you call the setter.

function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// count is 0 for this whole render. All three read the SAME snapshot (0),
// so this sets state to 0 + 1 = 1, three times. Result: 1, not 3.
}
return <button onClick={handleClick}>{count}</button>;
}

To update based on the latest pending value, pass an updater function — React runs them in order against the evolving value:

setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);
// Each receives the previous pending value: 0→1→2→3. Result: 3.

The mental model: a re-render is a fresh call of your function with a new snapshot. The old render’s variables are gone; the new render sees the new values.

Because state is a snapshot React compares by identity, you must never mutate it. Create new objects/arrays instead:

// ❌ Mutation — same array reference, React may not re-render, and it's a bug.
todos.push(newTodo);
setTodos(todos);
// ✅ New reference — React sees a change and re-renders.
setTodos([...todos, newTodo]);

Mutating state breaks React’s ability to detect changes (it compares references) and corrupts the snapshot model. Always produce a new value.

A component’s render — everything it does while computing its returned JSX — must be pure: given the same props and state, it returns the same output and causes no side effects. During render you must not:

  • mutate props, state, or any pre-existing variable/object;
  • write to the DOM, make network requests, or start timers;
  • read or write anything outside the function that could change.
flowchart LR
  inputs["props + state
(snapshot)"] --> render["render (pure):
compute JSX only"]
  render --> out["same inputs → same JSX"]
  render -. NOT here .-> se["side effects:
DOM, network, timers"]
  se --> eff["→ event handlers & effects"]
Pure render in, side effects out

Side effects belong in event handlers (things that happen on interaction) and effects (useEffect, for synchronizing with external systems) — never in the render body. This purity is what lets React call your component whenever it likes.

In development, React’s <StrictMode> deliberately calls your component function twice (and runs effects setup→cleanup→setup). This is not a bug — it’s a detector. If your render is pure, calling it twice produces identical output and nothing breaks. If you accidentally mutate something or rely on a side effect during render, the double-invocation surfaces the bug immediately, in development, instead of as a mysterious glitch in production.

Inside one event handler, `setCount(count + 1)` is called three times with count = 0. What is the result?
How do you correctly increment state three times in one handler?
Why must you not mutate state (e.g. `todos.push(x); setTodos(todos)`)?
What is StrictMode’s double-render for?