State & useState
useState gives per-render snapshots; use functional updates, lazy init, batching, and immutable updates to avoid stale-state bugs.
useState basics 🎯#
useState returns a [value, setter] pair. Calling the setter schedules a re-render; it does not mutate the variable in place.
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
State is a snapshot per render 🎯#
Each render captures its own count value — a constant for that render. The value doesn't change mid-render; a new render with a new value is what updates the screen. This explains the classic "off by one" bug:
function handleClick() {
setCount(count + 1); // count is 0 this render
setCount(count + 1); // still 0 → both set to 1, NOT 2
console.log(count); // logs 0 — still the snapshot value
}
Functional updates 🟢#
When the next state depends on the previous, pass an updater function. React applies them in sequence against the latest queued value.
setCount(c => c + 1);
setCount(c => c + 1); // now correctly results in +2
Rule of thumb: if new state derives from old state, use the functional form. Avoids stale-closure bugs entirely.
Stale state ⚠️#
Closures (event handlers, setTimeout, effects) capture the state value from the render they were created in. A delayed callback sees the old snapshot:
function Timer() {
const [n, setN] = useState(0);
const start = () => setTimeout(() => setN(n + 1), 3000); // captures n at click time
// fix: setN(prev => prev + 1)
}
Lazy initialization 🟢#
If the initial value is expensive to compute, pass a function so it runs only on the first render, not every render.
// ⚠️ runs on EVERY render, result discarded after first
const [state, setState] = useState(expensiveInit());
// ✅ runs once
const [state, setState] = useState(() => expensiveInit());
Automatic batching (React 18) 🎯#
React 18 batches all state updates in the same tick — including inside promises, setTimeout, and native event handlers — into a single re-render. (Pre-18, only React event handlers batched.) Use flushSync from react-dom to opt out when you need a synchronous DOM update.
async function save() {
setLoading(true);
await api();
setLoading(false); // batched with any sibling updates → one render
setDone(true);
}
Updating objects & arrays immutably ⚠️#
State must be replaced, not mutated — React compares by reference (Object.is) to decide whether to re-render. Mutating the same object means the reference is unchanged, so the UI may not update.
// ❌ mutation — same reference, no re-render
user.name = "Ada"; setUser(user);
todos.push(newTodo); setTodos(todos);
// ✅ new references via spread / array methods
setUser({ ...user, name: "Ada" });
setTodos([...todos, newTodo]);
setTodos(todos.map(t => t.id === id ? { ...t, done: true } : t));
setTodos(todos.filter(t => t.id !== id));
Render cycle#
flowchart LR
A["setState(next)"] --> B["schedule re-render"]
B --> C["React calls component()"]
C --> D["new snapshot of state/props"]
D --> E["diff Virtual DOM"]
E --> F["commit minimal DOM changes"]
Interview Q&A#
Q1. Why does calling setCount(count+1) twice in one handler only add 1?
count is a fixed snapshot for that render, so both calls compute the same value. Use functional updates setCount(c => c+1) to accumulate.
Q2. When should you use the functional updater form? Whenever the next state depends on the previous value, or inside async/delayed callbacks — it reads the latest queued state and avoids stale closures.
Q3. What is lazy initialization and why use it?
Passing a function to useState(() => init()) so the expensive initializer runs only on the first render instead of every render.
Q4. What changed about batching in React 18?
React 18 batches state updates everywhere — including timeouts, promises, and native events — not just inside React event handlers, reducing re-renders. Opt out with flushSync.
Q5. Why won't arr.push(x); setArr(arr) re-render?
It mutates the same array reference. React's Object.is comparison sees no change and skips the update. Create a new array: setArr([...arr, x]).