skip to content

In a React useReducer, what is the difference between dispatching { type: 'setStatus', value: 'loading' } and dispatching { type: 'submitted' }, and why do reviewers push for the second style?

level: seniorimportance: should knowfreq 42%

answer

  1. setter versus record of what happened
  2. caller decided vs reducer decides
  3. one gesture, one dispatch
  4. payload only what cannot be derived
  5. field-change actions are a fair exception

basics

~20 s

The first is a remote-control setter: the component decides the new state and the reducer just writes it. The second names what happened and lets the reducer decide the consequences, so the rules live in one place and each user interaction is one action.

solid answer

~50 s

`setStatus` is a setter in disguise — the caller already decided the answer, so the reducer is just a write channel and all the real logic stays scattered in the components. `submitted` names an event, so the reducer owns the consequences: it can set status, clear the previous error, and reset the retry count in one place. Two things follow. First, when three different components can trigger a submit, event-shaped actions keep the rules identical for all of them; setter-shaped actions mean three places to update when a rule changes. Second, one user interaction should be one dispatch — a handler that fires three setter actions in sequence has moved the transition back into the component, and the intermediate states it produces are real states that could render. I name actions in the past tense after what the user or system did, keep the payload to what the reducer cannot derive, and let the reducer decide everything else.

code

javascript · 23 lines
javascript
// Setter-shaped: the rule lives at every call site
function onSubmitSetters(dispatch, state) {
  dispatch({ type: 'setStatus', value: 'loading' });
  dispatch({ type: 'setError', value: null });
  dispatch({ type: 'setAttempts', value: state.attempts + 1 });
}

// Event-shaped: the rule lives once, in the reducer
function onSubmitEvent(dispatch) {
  dispatch({ type: 'submitted' });
}

function reducer(state, action) {
  switch (action.type) {
    case 'submitted':
      return { ...state, status: 'loading', error: null, attempts: state.attempts + 1 };
    default:
      return state;
  }
}

console.log(reducer({ status: 'error', error: 'boom', attempts: 1 }, { type: 'submitted' }));
// { status: 'loading', error: null, attempts: 2 }

go deeper

for a junior

Know that an action is an object with a type describing what happened, and that the reducer — not the handler — decides what the state becomes.

for a middle

Contrast the two shapes concretely: a setter action leaves the rules in the component, an event action moves them into the reducer so every call site behaves the same. Explain what belongs in the payload.

for a senior

Show the review instincts: multiple dispatches per gesture, payloads computed from state in the component, one action per field. Argue why event-shaped actions make the reducer's tests test real rules.

for a principal

Own the convention across a codebase — naming, payload shape, where impure values are captured — and articulate the honest exception for plain field changes so the rule does not become dogma teams route around.

## Two philosophies of what an action is An action can be a **command to write state** or a **record of something that happened**. The shape looks similar; the consequences are not. ```js // setter-shaped: the caller decided dispatch({ type: 'setStatus', value: 'loading' }); dispatch({ type: 'setError', value: null }); dispatch({ type: 'setAttempts', value: attempts + 1 }); // event-shaped: the reducer decides dispatch({ type: 'submitted' }); ``` In the first version, the reducer is a dumb writer and every rule about what a submit *means* lives in the click handler. In the second, the handler reports an event and the reducer owns the rule: ```js case 'submitted': return { ...state, status: 'loading', error: null, attempts: state.attempts + 1 }; ``` ## Why the event shape wins **One place to change a rule.** The moment a second trigger exists — a keyboard shortcut, a retry button, an auto-submit on blur — setter-shaped actions duplicate the sequence at each call site. They drift. Someone adds "clear the error on submit" to two of the three, and the third keeps showing a stale error. An event-shaped action makes all three call sites identical by construction. **One interaction, one dispatch.** If a handler fires three actions in a row, it has re-implemented the transition in the component. Worse, the intermediate values are genuine states that a concurrent render could observe. A single action means the state moves atomically from one valid configuration to the next. **The action log becomes readable.** A sequence of `submitted`, `succeeded`, `dismissed` tells you what the user did; a sequence of `setStatus`, `setError`, `setData` tells you only what fields were touched. That difference matters when you are reading a bug report, an analytics stream, or React DevTools. **The reducer becomes worth testing.** Asserting that `setStatus` sets the status proves nothing — it tests the assignment you just wrote. Asserting that `submitted` clears the previous error and increments attempts tests an actual business rule. ## How to name and shape them - **Name after the event, from the perspective of what happened**: `submitted`, `retried`, `dismissed`, `messageReceived`, `tokenExpired`. Past tense reads naturally and discourages the setter instinct. Some codebases use a domain prefix (`cart/itemAdded`) for grepability. - **Payload carries only what the reducer cannot derive.** The id of the clicked row, the text the user typed, the response body. Not the computed next state — that is the reducer's job. Not values already in `state` — the reducer is handed those. - **Impure values go in the payload, computed at dispatch time.** Timestamps and generated ids belong on the action so the reducer stays pure and testable. - **Keep the discriminator consistent.** Use `type` as a string and a stable payload convention across the reducer; mixed shapes make the switch hard to read and hard to type. ```js dispatch({ type: 'itemAdded', id: item.id, at: Date.now() }); ``` ## Where setter-shaped actions are still fine Dogma is not the answer. A controlled input dispatching `{ type: 'fieldChanged', field: 'email', value }` is a setter in spirit, and it is the right call — there is no domain event richer than "the user typed", and a per-field action type would be noise. The test is whether the reducer has a decision to make. If the answer is genuinely "write this value here, no rules attached", a generic field action is honest. If there *are* rules — clear the dependent field, reset validation, mark the form dirty — put them in the reducer and name the action after the event. ## The signal that you have it wrong Watch for these in review: - A handler that dispatches more than once for a single user gesture. - An action whose payload was computed from `state` in the component — the reducer had that state already. - One action type per state field, mirroring the `useState` calls you just removed. That is a reducer in shape only; you kept the scattered logic and paid for the indirection. - Conditionals in the component deciding *which* action to dispatch based on current state. That decision belongs in the reducer, which sees the current state anyway. When actions are events, the component says what happened, the reducer says what it means, and the two never have to argue.

  • A handler dispatches three actions in a row for one button click. What is the concern?
    The transition has moved back into the component, so the rule is duplicated at every call site that needs the same sequence, and the intermediate states are real states React could render. Collapse them into one event-shaped action and let the reducer write the whole next state atomically. If the three genuinely represent three separate events, that is fine — but then they should not be triggered by one gesture.
  • Is a generic { type: 'fieldChanged', field, value } action a design smell?
    Not by itself. For a plain controlled input there is no richer event than "the user typed", and one action type per field would be noise. It becomes a smell when the change carries rules — clearing a dependent field, resetting validation, marking the form dirty — because those rules then leak into the handler instead of living in the reducer.
  • How does action design change what the reducer's tests are worth?
    With setter-shaped actions, a test asserts that setStatus sets status — it verifies an assignment you can read in one line. With event-shaped actions, a test asserts that submitted clears the previous error and increments the attempt count, which is a real rule that can regress. Naming actions after events is what makes the reducer's test suite meaningful rather than tautological.

saying these in an interview costs you the question

  • One action type per state field, mirroring the setters
  • Computing the next state in the handler and passing it in
  • Dispatching several actions for one user click
  • Action names should be imperative commands like setX
  • Every payload should carry the full next state object

context