Engrain by ByteRush

RCT.03.02 Mid-level· 11 minCore — asked often

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);
}
This example uses Node.js, so it runs in our checks rather than in your browser.

Output (verified)

useState(loadDraft()) → loadDraft calls: 4
useState(() => loadDraft()) → loadDraft calls: 1
Five booleans vs one reducerStep 1 of 4

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
    1. The form keeps five useStates: values, errors, isSubmitting, isSuccess, errorMessage. A submit must update four of them.
    2. One handler forgets to reset isSuccess, so the form shows "Saved" and a spinner together.
    3. With useReducer, every transition is one case: submit → status "submitting", errors cleared, in one place.
    4. 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?

    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.”

    Stays on your device. Nothing is uploaded.
    Follow-up When should you use useReducer instead of useState?
    When state values change together, transitions are many or complex, or you want to test the logic apart from the component.
    Follow-up Should you store a selected item, or only its id?
    Only the id. Storing a copy duplicates the item, and the copy goes stale when the list changes.
    Follow-up Why must a reducer be pure?
    React may call it more than once (for example in Strict Mode), so it must only compute the next state from its inputs.

    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.
    • canSubmit is computed from state, never stored.
    Your solution
    Ctrl/⌘ + Enter

    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
    Group values that always change together, such as x and y of a position; keep unrelated values in separate states. With object state, always copy: 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
    Return the same object when nothing changes, so React can skip work. Never mutate the old state; never call APIs or timers inside the reducer.
    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 useReducer instead of useState?