Lesson 74 of 80
A pile of booleans (isLoading, isError, hasData...) can represent states that should never coexist. A state machine makes 'impossible' states actually impossible to write.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A finite state machine is a system that's always in exactly one named state out of a fixed set, and moves between states only through explicit, named transitions. Instead of isLoading, isError, and hasData as three separate booleans that could — through a typo, a race condition, or a missed reset — all end up true at once, you have one variable, status, that's "idle", "loading", "success", or "error", never more than one at a time, because there's only one variable to set.
The transitions matter as much as the states. A machine doesn't just say what states exist — it says which moves are legal from each state. You can't go from "idle" straight to "success"; you have to pass through "loading" first, because that's the only transition the machine defines out of "idle".
const transitions = {
idle: { FETCH: "loading" },
loading: { RESOLVE: "success", REJECT: "error" },
success: { FETCH: "loading" },
error: { FETCH: "loading" },
};
function createMachine(initial) {
let state = initial;
return {
get state() {
return state;
},
send(event) {
const next = transitions[state]?.[event];
if (!next) {
console.warn(`No transition for "${event}" from "${state}"`);
return state;
}
state = next;
return state;
},
};
}
const fetchMachine = createMachine("idle");
fetchMachine.send("FETCH"); // "loading"
fetchMachine.send("RESOLVE"); // "success"
fetchMachine.send("FETCH"); // "loading" again, re-fetching from success
fetchMachine.send("RESOLVE"); // "error" attempt would be ignored here — no such event from "loading" in this call, but REJECT is valid
transitions is the whole spec, in one object: for each state, which events are legal and where they lead. send looks up the current state's row, and if the event you're sending isn't a key in that row, nothing happens except a warning — the machine can't be pushed into an undefined or contradictory state because there's no code path that lets it. Compare that to three independent booleans: nothing stops isLoading = true and hasData = true from both being set by two callbacks racing each other, because nothing ever enforced that they're mutually exclusive in the first place.
FETCH but can't directly RESOLVE again — enumerating illegal moves is what prevents bugs, not just naming the legal states.playing/paused/buffering), drag interactions, and wizard flows all have the same shape: a small number of named states, a fixed set of legal moves between them. Any time you catch yourself drawing boxes and arrows on paper to explain a feature, that diagram probably already is the state machine.transitions above is a real, legitimate implementation — you don't need a library. Libraries like XState add useful things (nested/parallel states, side effects tied to transitions, visualizers) but the core mechanism, a lookup table plus a send function, is genuinely this simple for most UI needs.state here is a variable closed over by both the state getter and send — the same closure mechanism that keeps debounce's timer alive, just holding a single current state string instead. The discipline isn't a new language feature; it's choosing to route every mutation of that closed-over variable through one function (send) instead of setting flags directly from scattered call sites.