In React, what signals tell you a component should move from several useState calls to a single useReducer, and what do you actually gain by doing it?
answer
- several fields change on one event
- impossible combinations of independent flags
- logic repeated across handlers
- one pure transition function, testable
- dispatch is stable, not fewer renders
basics
~20 sMove to useReducer when several state fields change together on one event, when the next value depends on the previous one, or when the same update logic is repeated across handlers. You gain a single pure transition function you can test on its own.
solid answer
~50 sI look for three signals. First, fields that always change together — `status`, `data` and `error` as three separate `useState` calls means every handler has to remember to set all three, and forgetting one produces impossible states like "loading with a stale error". Second, updates whose next value depends on the previous state in a non-trivial way. Third, the same update logic duplicated across several handlers or passed down as a bundle of callbacks. What I gain is that the *how* moves out of the components: handlers only say what happened via `dispatch`, and one pure `reducer(state, action)` owns every transition, which I can unit-test as a plain function with no rendering. `dispatch` also has a stable identity, so passing it down doesn't invalidate memoized children. What I do *not* gain is fewer re-renders — that is not the reason to switch.
code
javascript · 15 linesfunction reducer(state, action) {
switch (action.type) {
case 'submitted':
return { status: 'loading', data: null, error: null };
case 'succeeded':
return { status: 'success', data: action.payload, error: null };
case 'failed':
return { status: 'error', data: null, error: action.error };
default:
return state;
}
}
console.log(reducer({ status: 'error', data: null, error: 'boom' }, { type: 'submitted' }));
// { status: 'loading', data: null, error: null }go deeper
Be able to say what useReducer is for in one sentence: handlers dispatch actions describing what happened, and one function computes the next state. Know that it is an alternative to useState, not a global store.
Name the concrete signals — fields that change together, next-state-depends-on-previous, duplicated update logic — and show the impossible-state bug that separate booleans produce. Explain that the reducer is a plain testable function.
Show judgment about when the indirection is not worth it, and be ready to correct the performance myth: batching already collapsed the setters, so the win is maintainability. Talk about how you would migrate an existing component incrementally.
Frame it as a codebase-level convention: where reducers live, how they are tested, when a component's state graph has outgrown local state entirely, and the cost of teams reaching for a reducer reflexively where two useState calls would read better.
## The real difference: where update logic lives Both `useState` and `useReducer` store state in the same place — React's hook list for that component instance. The difference is architectural, not mechanical. With `useState`, the logic that computes the next state is scattered across the event handlers that call the setters. With `useReducer`, the handlers describe *what happened* and a single function decides what the state becomes: ```js const [state, dispatch] = useReducer(reducer, { status: 'idle', data: null, error: null }); // handler onClick={() => dispatch({ type: 'submitted' })} ``` The component gets smaller and more declarative; the state logic gets concentrated and testable. ## The signals that justify the move **1. Fields that change together.** This is the strongest signal. Three `useState` calls for `status`, `data` and `error` mean every handler must remember to update all three in the right combination. Miss one and you render a combination that should not exist — an error message next to a spinner, or stale data under a fresh error. A reducer writes the whole next state in one place, so each transition is exhaustive by construction: ```js function reducer(state, action) { switch (action.type) { case 'submitted': return { status: 'loading', data: null, error: null }; case 'succeeded': return { status: 'success', data: action.payload, error: null }; case 'failed': return { status: 'error', data: null, error: action.error }; default: return state; } } ``` **2. The next state depends on the previous one in a non-trivial way.** A counter is fine with `setCount(c => c + 1)`. But "add this item unless it is already in the cart, in which case bump its quantity and recompute the total" is a transition, and transitions read better as a named action than as three chained updater callbacks. **3. The same update appears in several handlers.** If `handleCancel`, `handleEscape` and `handleBackdropClick` all run the same four setter calls, that repetition is a reducer case waiting to be named `'dismissed'`. **4. You want to test the logic without rendering.** A reducer is an ordinary function of `(state, action) -> state`. You can assert on it directly, with no component, no DOM, and no act-like ceremony: ```js expect(reducer({ status: 'idle' }, { type: 'submitted' }).status).toBe('loading'); ``` **5. You are passing a fistful of callbacks down the tree.** Handing children a single `dispatch` is usually cleaner than five `useCallback`-wrapped setters, and `dispatch` is stable so it never invalidates a memoized child. ## What you gain - **One pure transition function.** All the rules live in one switch, reviewable in one screen. - **Named events instead of anonymous writes.** `{ type: 'retried' }` says more in a stack trace, a log, or a React DevTools inspection than `setStatus('loading')`. - **A stable `dispatch`.** React guarantees the dispatch function's identity does not change across re-renders of that component, so it is safe to pass down and safe to omit from dependency arrays. - **Testability.** Plain-function tests cover the state rules; component tests then only need to cover wiring. ## What you do NOT gain (the common wrong answers) - **Not fewer re-renders.** One `dispatch` produces one update, but React already batches multiple state updates that happen in the same event, so three setters in one handler were already one render. Switching for performance reasons is a misdiagnosis. - **Not global state.** The state still lives in the component that called `useReducer`. Sharing it across the tree still requires putting it in context or an external store — the reducer alone changes nothing about scope. - **Not an escape from immutability.** The reducer must return a new object rather than mutating, exactly as a `useState` updater must. ## When `useState` is still the right answer Independent, small, unrelated values — an input's text, a disclosure's open flag, a hovered index. Wrapping two booleans that never interact in a reducer adds indirection (an action type, a switch case, a file) and buys nothing. The honest heuristic: start with `useState`, and move to `useReducer` the moment a handler starts coordinating several pieces of state at once.
- Does switching from several useState calls to useReducer reduce the number of re-renders?No, and that is the classic wrong reason to switch. React batches state updates that occur in the same event, so three setter calls in one handler already produced a single re-render. A dispatch produces one update too, so the render count is typically identical. The gain is legibility and testability of the transition logic, not render performance.
- How do you test the state logic after moving to a reducer?Directly, as a plain function. Export the reducer, call it with a state object and an action, and assert on the returned object — no rendering, no DOM, no async. Because the reducer must be pure, the same input always yields the same output, so each transition is a one-line test and edge cases like an unknown action type are trivial to cover.
- Does using useReducer make the state shared across components?No. The state lives in the component that called useReducer, exactly like useState. To share it you still have to lift it and expose it — commonly by putting state and dispatch into context, or by moving to an external store. Choosing a reducer is a decision about how updates are expressed, not about where state lives.
saying these in an interview costs you the question
- useReducer is only for large or Redux-style apps
- Switching to useReducer reduces re-renders
- useReducer makes the state global or shared
- Reducers may mutate state since React copies it
- One action per setter, mirroring each useState call