useState & Updates
The basics, precisely
Section titled “The basics, precisely”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.)
Batching: multiple setStates, one render
Section titled “Batching: multiple setStates, one render”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.
Updater functions vs values
Section titled “Updater functions vs values”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 totalsetCount(c => c + 1); // updater: uses the pending value; three of these add 3flowchart 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"]
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.
Lazy initialization
Section titled “Lazy initialization”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());State must stay immutable
Section titled “State must stay immutable”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]); // addsetItems(items.filter(i => i.id !== id)); // removesetItems(items.map(i => i.id === id ? { ...i, done: true } : i)); // updateFor 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).