skip to content

Your React app has several hand-rolled useReducer state machines for wizards and async flows. When does moving them to a statechart library such as XState pay for itself, and when is it over-engineering?

level: principalimportance: nice to knowfreq 28%

answer

  1. the reducer already gets you far
  2. state names glued from two concepts
  3. two things in flight at once
  4. the graph as a reviewable artifact
  5. a second mental model to staff

basics

~20 s

A statechart library pays off when flows outgrow a flat status list — nested states, concurrent regions, guards and delays repeated across features, or a graph non-engineers must review. For one component with four states and six transitions, a plain reducer is cheaper and clearer.

solid answer

~50 s

I look for the pressures a flat reducer handles badly. The first is hierarchy: a wizard step that itself has idle, validating and submitting sub-states forces a flat machine into `step2Validating`, `step3Validating` and so on, and the status list grows multiplicatively. The second is concurrency — two independent things in flight at once, like an upload progressing while the form is still editable — which a single status field cannot express. The third is repetition: guards, timeouts and entry/exit side effects re-implemented in every reducer. The fourth is communication: a statechart is a diagram a designer or QA can review, which matters when the flow *is* the product. Against that I weigh a real learning curve, a second mental model living alongside React state, and the bundle cost. For one component with four states I stay with `useReducer`; the day a flow's diagram no longer fits on a whiteboard, the library is earning its keep.

go deeper

for a junior

Know that a plain useReducer already gives you named states and controlled transitions, and that dedicated statechart libraries exist for larger flows. You are not expected to have adopted one.

for a middle

Be able to describe what a statechart adds beyond a flat machine — nested states and parallel regions — and give an example of a flow where a single status field stops being enough.

for a senior

Show the judgment: name concrete triggers for adoption in a real codebase, and be equally specific about the costs — learning curve, a second model beside React state, and the fact that side effects still have to obey React's effect model.

for a principal

Own the rollout decision. Set the criteria that qualify a flow, adopt selectively rather than wholesale, and be ready to defend the maintenance question: whether more than one engineer can safely change the machines six months later.

## What a hand-rolled reducer machine gives you already A `useReducer` with a state-first transition function already delivers most of the value people attribute to state machines: a named set of states, transitions decided by (state, event), illegal transitions ignored, and a pure function you can test without rendering anything. That baseline is cheap and it belongs in the codebase whether or not you ever adopt a library. The question is what a statechart adds beyond it. ## Pressure one: hierarchy A statechart's defining addition over a flat finite state machine is *nested* states. In a flat model, a three-step wizard where each step can be editing, validating or submitting has to name every combination: `step1Editing`, `step1Validating`, `step1Submitting`, `step2Editing`… The list grows as steps times sub-states, and every transition that applies to "any validating state" has to be written once per combination. Hierarchy lets the sub-machine be defined once and reused inside each step, and lets a transition attached to the parent apply to all its children. This is the single clearest signal. If you catch yourself writing state names that are two concepts glued together, you have hit the ceiling of a flat model. ## Pressure two: concurrency A single `status` field expresses exactly one thing at a time. When a screen has genuinely independent concerns in flight — an avatar uploading while the profile form is still being edited — a flat machine either invents combined states or, more commonly, quietly grows a second field beside the status and you are back to flags that can disagree. Statecharts model this as parallel regions: two orthogonal sub-machines, each with its own current state. You can approximate it with two reducers, and for two regions that is often the right call; past that the coordination between them is the thing you are hand-writing. ## Pressure three: repeated machinery Count what your reducers re-implement. Guards on transitions, delayed transitions ("if still submitting after five seconds, show the slow-network hint"), actions that must run on entering or leaving a state, retry with a bounded count. Each is a dozen lines the first time and a maintenance surface every time after. A library ships these as first-class concepts with one semantics instead of five slightly different in-house ones. ## Pressure four: the flow as a shared artifact Statecharts can be rendered as diagrams, and a definition that is data can be inspected by tooling. That matters when the flow is a product surface that a designer, a QA engineer or a compliance reviewer needs to reason about — onboarding, payments, KYC. "Here is the diagram, is this the intended behaviour?" is a conversation you cannot have over a nested `switch`. If nobody outside the team ever needs to see the graph, this benefit is worth nothing to you. ## The costs, stated honestly A statechart library is a second mental model beside React's own. Engineers who are fluent in hooks are not automatically fluent in statecharts, and half-learned adoption produces machines that are harder to read than the reducers they replaced. There is a bundle cost. There is the migration cost of flows that work today. And there is a subtler one: React still owns rendering and effects, so the machine's side effects have to be integrated with React's effect model rather than escaping it — the library does not exempt you from the Rules of React, and a machine that mutates or triggers effects during render is broken in exactly the same way a component would be. ## How I would actually decide I would not migrate a codebase wholesale. I would name the criteria — nested states, parallel regions, more than roughly a dozen transitions, or a flow that stakeholders outside engineering review — and adopt the library only for flows that meet them, keeping plain reducers for the rest. Then I would look after six months at whether the adopted machines are being maintained by more than the person who wrote them. A pattern that only one engineer can safely change is a liability regardless of how correct it is. The failure mode worth naming out loud is adopting the library and still writing a flat machine in it: you pay the learning curve and the bundle and get back what a `useReducer` already gave you.

  • What is the smallest signal that a flow has outgrown a flat reducer?
    State names that concatenate two independent concepts — `step2Validating`, `uploadingWhileEditing`. Each one means you are encoding a product of two dimensions into a single field, and the count grows multiplicatively as either dimension gains a value. That is the point at which hierarchy or parallel regions stop being theoretical and start removing real code.
  • Does adopting a statechart library change how side effects work in a React app?
    It changes where they are declared, not the rules they must obey. React still owns rendering, so effects triggered by a machine have to run through React's effect model rather than during render. A machine that fires a request as a side effect of computing the next state is breaking the same rule a component would be, and the library does not exempt it.
  • How would you introduce it to a team without a big-bang migration?
    Pick one flow that clearly meets the criteria — a multi-step onboarding, say — and implement only that with the library, leaving every other reducer alone. Write down the criteria that made it qualify so the next adoption is a decision, not a fashion. Then check after a couple of months whether more than one engineer can change it comfortably.

saying these in an interview costs you the question

  • Says every reducer should become a statechart
  • Rejects the library purely on bundle size
  • Cannot name what nesting or parallel states add
  • Claims the library replaces React state entirely
  • Adopts it and still writes a single flat status list

context