Skip to content
Intermediate10 min study

State modeling with reducers

Model related state transitions explicitly so impossible UI states become harder to represent.

Question progress0 / 10 completed
Start the lesson
State modeling with reducers visual explanation

WHAT YOU WILL BE ABLE TO DO

Learning outcomes

  • Explain state modeling with reducers in plain language.
  • Connect the behavior to the underlying browser or framework model.
  • Implement the core pattern and reason through edge cases.
  • Answer common follow-ups without relying on memorized phrases.

01Explain it simply

A reducer centralizes how state changes in response to named events. It is useful when several state fields change together or when a workflow has many transitions.

One-line definition: Model related state transitions explicitly so impossible UI states become harder to represent.

02Mental model

Think in events, not setters. The UI dispatches what happened; one pure function decides the next state. This makes transitions reviewable, testable, and replayable.

03Step by step

  • List the meaningful events in the workflow.
  • Design a state shape that represents valid situations.
  • Write a pure reducer for every event.
  • Keep effects outside the reducer.
  • Test transitions as input-state plus action equals output-state.

04Working example

TypeScript
type State = { status: 'idle' | 'saving' | 'saved' | 'error'; error?: string };type Action = { type: 'submit' } | { type: 'success' } | { type: 'failure'; message: string };
function reducer(state: State, action: Action): State {  switch (action.type) {    case 'submit': return { status: 'saving' };    case 'success': return { status: 'saved' };    case 'failure': return { status: 'error', error: action.message };  }}

The discriminated action union makes each transition explicit and lets TypeScript verify that payloads such as message are available only where required.

05Where it is used

  • Multi-step forms
  • Async request state
  • Complex selection and editing workflows
  • Undoable interactions

06Common mistakes

  • Using a reducer for one independent boolean
  • Performing fetches or mutations inside the reducer
  • Allowing vague set_state actions
  • Duplicating values that can be derived during render

07Interview answer

Explain the threshold: use local state for independent values; use a reducer when events coordinate multiple fields or transitions need explicit modeling.

Why must a reducer be pure?

React may call rendering logic more than once, and predictable state transitions require the same state and action to always produce the same result without external effects.

DDConcept deep dives

Deep dive 1

Model events before writing the reducer

Start by naming what can happen in the domain: the user submitted, the request succeeded, validation failed, or the flow reset. Those events form an action vocabulary. The reducer then defines the next state for each event. This avoids scattering coordinated field updates across handlers and makes the workflow reviewable without opening the UI.

  • Events describe facts; setField is only appropriate for genuinely field-oriented state.
  • Keep action payloads narrow and validated at the dispatch boundary.
  • A reducer centralizes transitions but does not need to own all component state.

Deep dive 2

Use state shapes that encode valid situations

Independent isLoading, data, and error values permit contradictory combinations. A discriminated union creates one representation for idle, loading, success, and error, each with only valid data. The reducer moves between those states, and exhaustive TypeScript checks reveal missing handling when the workflow grows.

  • Derive booleans such as isLoading from the status instead of storing duplicates.
  • Keep server data attached only to states where it is meaningful.
  • Decide deliberately whether old data remains visible during refresh.

Deep dive 3

Reducers stay pure; effects perform work

A reducer calculates state and must not fetch, write storage, read time, mutate inputs, or dispatch recursively. Event handlers or effects start external work and dispatch results. Pure transitions can be tested as plain input-output cases, replayed, logged, and reasoned about under repeated React renders.

  • Lazy initialization can compute the first state once from an input.
  • React provides a stable dispatch identity.
  • Complex reducer code may be split by domain, but cross-domain invariants still need one clear owner.

QAInterview questions and model answers

Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.

Intermediate · Conceptual · 1 min · Question 1When is useReducer preferable to several useState calls?Open model answer

Model answer

Use a reducer when multiple fields change together, a workflow has named transitions, or the next state depends on a structured event. Independent simple values remain clearer as separate state. Reducers improve modeling, not automatically performance.

Open question page →
Intermediate · Coding · 1 min · Question 2Why should reducer actions describe events?Open model answer

Model answer

Actions such as submitted or requestSucceeded record what happened, while vague setState actions leak implementation details. Event-shaped actions centralize invariants and make transitions readable, testable, and easier to evolve.

JSX
// Leaky: caller decides the mechanicsdispatch({ type: 'setLoading', value: true });dispatch({ type: 'setError', value: null });
// Event-shaped: reducer owns the transitiondispatch({ type: 'submitted' });
Open question page →
Intermediate · Conceptual · 1 min · Question 3Why must a reducer be pure?Open model answer

Model answer

The same state and action must produce the same next state without mutating inputs or causing effects. React can call rendering logic more than once, and pure reducers enable predictable testing and scheduling. Network requests and storage belong outside the reducer.

Open question page →
Advanced · Coding · 1 min · Question 4How do discriminated unions improve reducer design?Open model answer

Model answer

A literal type field narrows each action to its valid payload, and a union can model mutually exclusive states. Exhaustive checking catches forgotten transitions when a new event or state is added.

JSX
type Action =  | { type: 'submitted' }  | { type: 'succeeded'; order: Order }  | { type: 'failed'; message: string };
function reducer(state: State, action: Action): State {  switch (action.type) {    case 'succeeded': return { status: 'success', order: action.order };    case 'failed':    return { status: 'error', message: action.message };    // ...  }}
Open question page →
Intermediate · Conceptual · 1 min · Question 5Can dispatch safely be omitted from effect dependencies?Open model answer

Model answer

React guarantees a stable dispatch identity for useReducer, so including or omitting it does not cause an effect to rerun. Other reactive values used by the effect still belong in the list.

Open question page →
Intermediate · Coding · 1 min · Question 6How do you avoid impossible async states?Open model answer

Model answer

Use a tagged state union such as idle, loading, success with data, and error with a message rather than independent booleans and optional fields. The reducer then permits only explicit transitions between valid states.

JSX
// Impossible states are representable:{ isLoading: true, error: 'x', data: {...} }
// Only valid states are representable:type State =  | { status: 'idle' }  | { status: 'loading' }  | { status: 'success'; data: Data }  | { status: 'error'; message: string };
Open question page →
Advanced · Conceptual · 1 min · Question 7Can a reducer return the same state object?Open model answer

Model answer

Yes. If nothing changed, returning the existing reference lets React avoid work. Never mutate that object before returning it.

Open question page →
Intermediate · Conceptual · 1 min · Question 8What is lazy reducer initialization?Open model answer

Model answer

The third useReducer argument calculates initial state from the initial argument once, avoiding repeated expensive initialization during renders.

Open question page →
Advanced · Conceptual · 1 min · Question 9Should one application have one global reducer?Open model answer

Model answer

Not by default. Reducers should follow domain ownership; one giant reducer couples unrelated changes and broadens subscriptions without providing useful invariants.

Open question page →
Beginner · Conceptual · 1 min · Question 10How do you test a reducer?Open model answer

Model answer

Provide representative states and actions, assert exact next states and immutability, and cover invalid or repeated transitions without rendering a component.

Open question page →

SCScenario questions

Scenario 1

A checkout can simultaneously show isLoading, error, and success because they are separate booleans.

  1. List valid domain states and events.
  2. Replace contradictory flags with a discriminated state.
  3. Define transitions for submit, success, failure, retry, and reset.
  4. Test forbidden and repeated transitions.
Reveal worked answer

I would model one status discriminator with state-specific data, such as success carrying the order and error carrying the recoverable message. The reducer owns transitions, so a failure cannot leave success true. Effects perform the request and dispatch domain events.

Verify and go deeper