"Loading spinner still showing after an error", "submit fires twice", "back button lands in an impossible screen" — most UI bugs are really state bugs: the app drifted into a combination you never intended. A finite state machine kills that whole class by making the legal states and the transitions between them explicit data. You're in exactly one state; an event either has a defined transition out of it or it's ignored. Libraries like XState are built on this; here you'll write the tiny interpreter at its heart.
Implement finiteStateMachine(config) returning a machine with send, can, matches, and subscribe, supporting guards and entry actions. See statecharts.dev.
function finiteStateMachine(config) {
return { state, context, send, can, matches, subscribe };
}
const m = finiteStateMachine({
initial: 'idle',
context: { data: null },
states: {
idle: { on: { FETCH: 'loading' } },
loading: {
on: {
SUCCESS: { target: 'ok', action: (ctx, e) => { ctx.data = e.data; } },
ERROR: { target: 'fail', guard: (ctx, e) => e.fatal === true },
},
},
ok: {}, fail: {},
},
});
m.send('FETCH'); // -> 'loading'
m.send({ type: 'SUCCESS', data: 7 }); // -> 'ok', ctx.data === 7
on[event.type]. No entry → ignore the event (stay put). Events are strings or { type, ...payload }.guard, it fires only when guard(context, event) is truthy; otherwise the machine stays.action runs during the switch; the target state's entry runs on arrival. The initial state's entry runs once at creation.can, matches, subscribe — can(event) previews whether a transition would fire; matches(state) checks the current state; subscribe notifies on each real transition.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.