Detailed explanation
Use useReducer instead of useState when state transitions become complex enough that centralising them makes the component easier to understand. Typical signals include several related values changing together, many user actions affecting the same state, or rules that must remain consistent across updates. For example, a checkout flow may track the current step, selected delivery option, validation errors, and submission status. A reducer can describe how each event changes those values without scattering the rules across event handlers.
useReducer takes a reducer function and an initial state, then returns the current state and a dispatch function. Calling dispatch({ type: 'inc' }) requests an update; React processes the action by calling the reducer with the current state and that action. In the example, returning { ...s, n: s.n + 1 } creates the next state while preserving other fields. The reducer must be pure: it should calculate state without mutating existing objects, making network requests, or performing other side effects. React uses the returned value as the next state; returning an Object.is-equal value allows it to skip unnecessary rendering work.
The main advantage is explicit, testable transition logic. An action such as 'submissionStarted' can set loading, clear errors, and reset a success flag in one coherent transition. A reducer defined outside the component can be tested directly with a previous state and an action, without rendering React. This also makes business rules easier to review. However, reducers do not automatically validate transitions: you still need to encode rules such as ignoring submission while a request is already pending.
Prefer useState for simple, independent values such as whether a tooltip is open. Depending on previous state alone is not a reason to switch: setCount(c => c + 1) already handles that correctly. Likewise, multiple fields can live in one useState object. Interviewers generally look for a complexity-and-maintainability justification, not a claim that useReducer is inherently faster. It structures local state updates; it does not automatically provide global state, persistence, or asynchronous request handling.
Key points to remember
- Choose useReducer for many actions, coupled fields, or non-trivial transition rules.
- Dispatch describes what happened; the reducer calculates the next state.
- Keep reducers pure and update objects and arrays immutably.
- Extract reducers to test state transitions independently of React rendering.
- Keep useState for simple state; functional updates already support previous-state calculations.
Common mistakes candidates make
- Switching to useReducer merely because an update depends on previous state.
- Mutating state or performing API calls inside the reducer.
- Claiming useReducer automatically improves performance or makes state global.
Likely follow-up questions
Where should asynchronous work go when using useReducer?
Run asynchronous work in an event handler, an effect when synchronisation is needed, or a dedicated data-fetching layer—not inside the reducer. Dispatch actions such as 'requestStarted', 'requestSucceeded', and 'requestFailed' to represent the resulting state transitions.
Can useReducer replace Redux?
useReducer manages state for a component, and Context can expose that state and dispatch to descendants. It does not by itself provide Redux's store subscriptions, middleware, or development tooling, so the choice depends on application requirements.
Does dispatch update the state variable immediately?
No. Dispatch requests an update, so code in the current event handler still reads the state snapshot from that render. React processes queued actions to calculate state for a subsequent render.
Interview tip: Contrast a simple toggle with a multi-action form, then explain how a reducer keeps related updates consistent and directly testable.
