When the State Chart Pays Off

Why a React form with seven boolean flags shipped a flicker bug that statecharts would have surfaced before the first render, and the decision rule that says when this discipline earns its place.

By Jovani Pink April 15, 2026 8 min — Systems & Complexity Notes

Outcome focus: Reader can decide when a workflow is state-machine-shaped, replace boolean-flag explosion with a small statechart that names guards and transitions, and recognize statecharts as an architectural discipline rather than a UI utility.

Part 1 of 4. Part 2: XState, Actors, and What the Stately Argument Actually Buys. Part 3: State Machines in Python: from xstate-python to LangGraph. Part 4: State Machines in Go, Elixir, Swift, and Zig.

A React form had seven boolean flags. loading, error, success, dirty, retrying, submitting, and cancelled. The component checked them in different combinations to decide which message to show, which button to enable, and whether the toast had already been dismissed.

For one render after a refetch, retrying and success were both true. The retry button rendered next to the success toast for sixteen milliseconds. A user took a screenshot. The bug looked impossible. Going through the flags in pairs to find the collision took two engineers a full afternoon.

The fix took twenty minutes. The realization that the form was a state machine took longer.

What Boolean Flags Hide#

Seven boolean flags describe a system with one hundred and twenty-eight possible combinations. Most of them are invalid in any honest description of the workflow. The component never wanted loading and error to both be true. It never wanted success while retrying. The "valid" combinations were a small subset, but the type system did not know that.

CombinationValid?What the UI rendered
loading: true, error: false, success: falseyesspinner
loading: false, error: true, success: falseyeserror message + retry button
loading: false, error: false, success: trueyessuccess toast
loading: false, retrying: true, success: trueno, but happenedretry button + success toast
loading: true, error: true, ...noundefined

The last two rows are where the bug lived. The state was not "valid for one render" because of a race; it was representable in the type system, and the team had no language for "this combination should never exist."

A statechart names exactly which combinations are valid. The form has a finite number of states (idle, submitting, error, success, retrying) and a finite number of events (SUBMIT, RETRY, CANCEL, DISMISS). Transitions name which events move which state to which other state, with guards that name the conditions under which a transition is legal. The chart does not eliminate bugs, but it eliminates the category of bug where two flags can be true at once because they are no longer two flags.

The Failure Mode and the Fix#

I have seen this pattern fail in three flavors. The first is the flicker, where a render happens between two state updates and the user sees an impossible UI for one frame. The second is the dead state, where a transition is missing entirely and the form gets stuck because the developer forgot to handle a particular event in a particular state. The third is the silent corruption, where a guard was meant to be in place but was not, and the user advances through the flow with bad data.

The chart prevents all three with the same discipline: enumerate the states, enumerate the events, and force every state-event pair to either have a transition or be explicitly absent. There is nowhere for an impossible combination to hide because there is no boolean to flip out of band.

The corrected form looks like this when drawn:

The corrected form as a state chart. Five states, five events, named guards on the retry transition. The retry button only renders in the error state, never alongside the success toast.

The fix in code follows the chart. There is no "retrying" boolean. There is a single state value that is one of the five names. The retry button renders only when state === 'error'. The success toast renders only when state === 'success'. The flicker disappears because the two views can no longer be true at the same time.

The Tradeoff That Matters#

Statecharts are not free. The cost is the up-front work to enumerate the states and events before the form can be built incrementally. For a single boolean (loading versus loaded), the chart is more work than the boolean. For four or five interacting flags, the chart is less work than the bug fixes the flags will eventually cause.

The tradeoff is between deferring the modeling work and front-loading it. Deferring the work is faster on the first day and more expensive on the day the form needs to grow. Front-loading the work is slower on the first day and cheaper on the day the form grows.

The signals that a workflow is state-machine-shaped are concrete. Three or more interacting boolean flags. Transitions that depend on conditions ("retry only if attempt count is below three"). A natural diagram that has more than two boxes when you draw it on paper. Lifecycle steps that include "wait for an external thing to happen and resume." Any of those means the work is going to grow toward a chart whether or not the team draws one. The chart is the cheaper version of the growth.

The signals that a workflow is not state-machine-shaped are also concrete. A pure function that maps input to output. A simple loading boolean with no error path that the team would actually distinguish from "not loaded yet." A list of items where the only operation is add or remove and there is no per-item lifecycle. For these, the boolean or the function is the right shape, and forcing a chart through them is decoration.

A Decision Matrix You Can Lift#

This is the table I would keep in the codebase before reaching for a state machine library.

SignalWhat it looks likeMachine-shaped?
Three or more interacting boolean flagsloading && !error && retrying checks scattered across componentsyes
Conditional transitions"advance only if attempt count is below three," "submit only if validated"yes
Long-running steps with resume"wait for human approval," "wait for external webhook"yes
Hierarchical lifecycle"while editing, the autosave is independently in idle, saving, or saved"yes
Single boolean, no resume"loading vs. loaded," "expanded vs. collapsed"no
Pure transformationinput -> output with no intermediate statesno
Append-only listitems added, no per-item lifecycleno

A workflow that hits two or more "yes" rows almost always pays for the chart. A workflow that hits two or more "no" rows almost never does. Something in the middle is a judgment call, and that is where the team's principle stack matters more than the formalism.

Statecharts as Architecture, Not UI#

A state chart is not a UI library. It is an architectural discipline that happens to show up most often in UI code because UI code has the highest density of interacting flags. The same discipline applies to background jobs, agent workflows, document approval flows, payment pipelines, and any system where the work has lifecycle and the lifecycle has branches.

Two posts in this site already think in this discipline without naming it. The post on principle stacks as a way to make tradeoffs explicit describes ranked decision logic as a way to resolve conflicts between principles when they collide. That is what a guard condition is. The post on product-minded software architecture describes architecture as the place where product judgment becomes real, deciding which surfaces can share behavior. That is what compound and parallel states are. Neither post uses statechart vocabulary, but both are operating on state-chart-shaped problems.

The connection runs the other direction too. The series of agent posts on this site, especially the one on evaluating multi-agent workflows, names failure classes (handoff loss, retry drift, completion ambiguity, latency spiral) that map almost one-to-one onto state-chart concepts (missing transitions, guard condition errors, terminal-state ambiguity, timer state mismanagement). The post on sandboxed agents and production automation describes agents as having "a workspace, a control plane, state, recovery, and explicit execution boundaries." That is a state machine architecture diagram, written in agent vocabulary.

Statecharts are how teams formalize what those posts are gesturing at. They are not the only way. They are the way that has the most prior art, the most tooling, and the longest track record.

The Honest Limit of the Discipline#

Two cautions. The first: statecharts make invalid states visible, but they do not guarantee them absent. Runtime guards still matter. A transition that is "legal" in the chart can still fire at the wrong time if the guard condition is wrong, and the chart only protects the team from the structural mistake, not the logical one. The second: statecharts are easy to over-engineer. A team that draws a chart for every component will spend more time drawing than building, and the discipline will start looking like decoration. The discipline earns its place where the cost of the bug exceeds the cost of the chart, and that boundary is a judgment, not a rule.

For the form that started this post, the chart was right. Five states, five events, a half-day of modeling work that prevented the flicker bug, the dead-state bug, and the silent-corruption bug. The team paid for the discipline once, and the form has not had a state-related bug since. That is the bar. Not "every component is a chart." Just "this component is."

Close#

The next two posts in this series go deep into XState 5 in TypeScript and into the Python state-machine ecosystem, which is where I work most often. The post after that compares how Go, Elixir, Swift, and Zig express state machines, because the runtime guarantees each language ships change which idioms are honest and which are decoration.

If you have a component, a workflow, or an agent that hits three or more rows of the decision matrix above, draw the chart this week. Not in a meeting. In the codebase. The chart is shorter than the bug-fix you will write next month if you do not.

Back to all writing
On this page
  1. What Boolean Flags Hide
  2. The Failure Mode and the Fix
  3. The Tradeoff That Matters
  4. A Decision Matrix You Can Lift
  5. Statecharts as Architecture, Not UI
  6. The Honest Limit of the Discipline
  7. Close