Skip to content

useState & Updates

useState returns a pair: the current value for this render, and a setter that schedules a re-render with a new value.

const [count, setCount] = useState(0);
// count → the value for THIS render (a snapshot)
// setCount(next) → schedule a re-render where count is `next`

Calling the setter does not change count immediately — it asks React to render again. In the next render, useState(0) returns the updated value. (The Foundations “state is a snapshot” lesson is the mental model; this lesson is the mechanics.)

React batches state updates that happen in the same event. Several setter calls in one handler produce a single re-render, not one per call.

function handleClick() {
setCount(c => c + 1);
setFlag(true);
setName("Ada");
// → ONE re-render, with all three updates applied. React batches them.
}

Since React 18 this batching also covers updates inside promises, timeouts, and native event handlers — batching is automatic everywhere. Fewer renders, consistent UI.

Pass a value when the next state doesn’t depend on the previous. Pass an updater function when it does — React applies updaters in order against the latest pending value.

setCount(count + 1); // value: uses the snapshot; three of these still add 1 total
setCount(c => c + 1); // updater: uses the pending value; three of these add 3
flowchart LR
  start["pending = 0"] --> u1["c => c + 1
→ 1"]
  u1 --> u2["c => c + 1
→ 2"]
  u2 --> u3["c => c + 1
→ 3"]
  u3 --> done["next render: count = 3"]
Updater functions apply against the evolving value

Rule of thumb: if you call the same setter more than once in a handler, or the new value is derived from the old, use the updater form.

The argument to useState is only used on the first render, but React still evaluates it every render (then ignores it). If computing the initial value is expensive, pass a function — React calls it only once.

// ❌ createInitialTodos() runs on EVERY render (result discarded after the first).
const [todos, setTodos] = useState(createInitialTodos());
// ✅ Lazy: the function runs only on the first render.
const [todos, setTodos] = useState(() => createInitialTodos());

React decides whether to re-render by comparing the reference. Mutating an object or array in place keeps the same reference, so React may skip the update — and it corrupts the snapshot model. Always produce a new value.

// ❌ Mutation — same reference, unreliable render.
user.name = "Ada";
setUser(user);
// ✅ New object.
setUser({ ...user, name: "Ada" });
// ✅ New arrays for add / remove / update.
setItems([...items, newItem]); // add
setItems(items.filter(i => i.id !== id)); // remove
setItems(items.map(i => i.id === id ? { ...i, done: true } : i)); // update

For deeply nested state, spreading gets tedious — that’s a signal to flatten the shape, use an updater library like Immer, or reach for useReducer (a later lesson).

What does calling a useState setter do?
When should you use the updater form `setX(x => ...)`?
Why pass a function to useState (`useState(() => expensive())`)?
Why must you update object/array state immutably?