Loading, failed and successful — all at once
A checkout button tracks its request with booleans: isLoading, isError, isSuccess. Three booleans make eight combinations, and only four make sense. Sooner or later a bug produces the others: a spinner *and* an error, or a success message while the second click is still sending a duplicate order to the kitchen.
The field guide recommends useReducer once updates get complex, because every change becomes a named action handled in one place. Push that one step further and the reducer becomes a finite state machine: one status that can only be one thing at a time, and a table of which events are allowed in which status. Impossible states stop being bugs you test for — they can't be represented at all.
States, events and a transition table
A reducer is a pure function (state, action) => newState. As a state machine it first asks: *is this event allowed in the current status?* If not, it returns the same state object, and React skips the re-render. The allowed moves live in a plain object you can read at a glance — or draw as a diagram for your team.
const MACHINE = {
idle: { SUBMIT: "submitting" },
submitting: { RESOLVE: "success", REJECT: "failure" },
failure: { RETRY: "submitting", CANCEL: "idle" },
success: { RESET: "idle" },
};
function orderReducer(state, event) {
const next = MACHINE[state.status][event.type];
if (!next) return state;
// …build the next state for this event
}A traffic light: A light is red, amber or green — never two at once — and it only moves in a fixed order. No button press makes it jump from red to 'green and red'. A state machine gives your UI the same guarantee.