useState and useReducer: structuring state so bugs cannot happen
Lazy initial state, one object vs several states, when useReducer beats useState, keeping reducers pure, and the state-structure rules: avoid redundant and duplicated state, and derive values during render.
Comes up in: React, Coding
Learn#
Nidhi’s sign-up form sometimes shows “Saved” and a spinner at the same time. It has five useState calls, and each handler updates some of them. One handler forgot one.
useState is right for independent values. When several values change together, a reducer puts every change in one function, (state, action) => newState, and useReducer runs it. Each action is one case, so a submit always updates everything it should.
State structure matters more than the hook. Store one status field instead of several booleans, so impossible combinations cannot exist (TS.02.02). Do not store what you can calculate: derived state such as “can submit” or “full name” is computed during render.
An initial value written as a call runs on every render, even though React uses it only once. Pass a function instead, called lazy initialisation:
import { GlobalRegistrator } from "@happy-dom/global-registrator";
GlobalRegistrator.register(); // a DOM for Node; in a browser you already have one
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
const { createElement: h, useState, act } = await import("react");
const { createRoot } = await import("react-dom/client");
let reads = 0;
function loadDraft() { reads++; return "saved draft"; } // imagine parsing localStorage
let bump;
function Editor({ lazy }) {
const [draft] = useState(lazy ? () => loadDraft() : loadDraft());
const [, setTick] = useState(0);
bump = () => setTick((t) => t + 1);
return h("p", null, draft);
}
for (const lazy of [false, true]) {
reads = 0;
const root = createRoot(document.createElement("div"));
await act(() => root.render(h(Editor, { lazy })));
for (let i = 0; i < 3; i++) await act(() => bump());
console.log(lazy ? "useState(() => loadDraft())" : "useState(loadDraft())", "→ loadDraft calls:", reads);
}Output (verified)
useState(loadDraft()) → loadDraft calls: 4 useState(() => loadDraft()) → loadDraft calls: 1
State
- isSubmitting = true
- isSuccess = true (from last time!)
Page
empty
The form keeps five useStates: values, errors, isSubmitting, isSuccess, errorMessage. A submit must update four of them.
Read the steps as text
- The form keeps five useStates: values, errors, isSubmitting, isSuccess, errorMessage. A submit must update four of them.
- One handler forgets to reset isSuccess, so the form shows "Saved" and a spinner together.
- With useReducer, every transition is one case: submit → status "submitting", errors cleared, in one place.
- status is one field with four values, so "saved and saving" cannot exist. canSubmit is derived, never stored.
Predict#
Predict first
State holds firstName, lastName and fullName. The user edits firstName. What can go wrong?
Storing fullName duplicates information, so every change must remember to update it. Compute it during render, const fullName = first + " " + last, and it can never be wrong. It is tempting to store it “for convenience”.
Interview angle#
Say it like this — about 30 seconds
“I use useState for independent values and useReducer when several values change together or the next state depends on the previous one in many ways, such as a multi-step form. The reducer keeps all transitions in one pure function that I can unit-test, and dispatch has a stable identity, so it is easy to pass down. For structure, I avoid redundant and duplicated state, prefer one status field over many booleans, derive values during render, and use lazy initialization for expensive starting values.”
Follow-up When should you use useReducer instead of useState?
Follow-up Should you store a selected item, or only its id?
Follow-up Why must a reducer be pure?
Practice#
Guided
Write the reducer for a sign-up form, plus a derived canSubmit.
Done when
- Each action updates every related field in one place, without mutating.
- A second submit while submitting changes nothing.
canSubmitis computed from state, never stored.
Remember#
Try to answer each card before you open it. Then add them to your reviews.
What is lazy initial state?
Passing a function to useState so the initial value is computed only once.
When does useReducer beat useState?
When several values change together or transitions are complex.
What is derived state?
Values computed from existing state or props during render, not stored.
Why prefer one status field over several booleans?
It makes impossible combinations unrepresentable.
Go deeper
Four layers. Stop at the one your interview needs; each ends with a quick check.
The core ideaEvery round
setForm({ ...form, name }).Check yourself: One object state or several useState calls?
Group values that change together; keep unrelated values separate.
How it works underneathSenior rounds
dispatch is stable across renders, so passing it to children or effects never causes extra renders. React passes the latest state into the reducer, so there are no stale values inside it Source: React: useReducer.
Check yourself: Does dispatch change identity between renders?
No. It is stable, so it is safe in deps and props.
Edge cases and trapsWhere people slip
Check yourself: What happens if a reducer returns the same state object?
React can bail out and skip re-rendering children.
In productionStaff-level answers
Keep state minimal: no redundant values you can compute, no duplicates (store ids, not copies), and no deep nesting you must copy on every update Source: React: Choosing the state structure.
Check yourself: Name two principles for structuring state.
Avoid redundant state; avoid duplicated state (store ids, not copies).
Finished the five steps? Mark it, and its cards join your reviews.
Last checked 2026-11-01. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.
Interview questions this answers
- When should you use
useReducerinstead ofuseState?