Compact finite state machines
Build tiny, typed, flat state machines with xstate/fsm.
XState v6 is in alpha
Use xstate/fsm when a plain switch statement would work, but you want a
declarative transition table, type-safe events, and a machine definition that
can be visualized or upgraded to a full XState machine later.
The xstate/fsm entry typechecks independently: importing it does not require
importing xstate or loading the full statechart type definitions.
Without schemas, the API is a pure transition table:
import { createFSM } from 'xstate/fsm';
const machine = createFSM({
initial: 'inactive',
states: {
inactive: { on: { toggle: 'active' } },
active: { on: { toggle: 'inactive' } }
}
});
let state = machine.initialState;
[state] = machine.transition(state, { type: 'toggle' });
console.log(state.value); // 'active'transition returns a [nextState, effects] tuple. FSMs have no effects, so
effects is always an empty array. xstate/fsm and full XState actor logic
share the same (snapshot, event) => [snapshot, effects] protocol.
For typed context and event payloads, use the canonical v6 setup shape:
import { setup, types } from 'xstate/fsm';
const machineSetup = setup({
schemas: {
context: types<{ count: number }>(),
events: {
increment: types<{ by: number }>(),
reset: types<{}>()
}
}
});
const machine = machineSetup.createFSM({
initial: 'idle',
context: { count: 0 },
states: {
idle: {
on: {
increment: ({ context, event }) => ({
context: { count: context.count + event.by }
}),
reset: { context: { count: 0 } }
}
}
}
});Declare per-state context schemas when the snapshot should be a discriminated union. They refine an existing root context schema in the same way as full XState machines, so root fields remain available:
type User = { id: string };
const machineSetup = setup({
schemas: { events: { resolve: types<{ user: User }>() } },
states: {
loading: { schemas: { context: types<{ status: 'loading' }>() } },
loaded: {
schemas: {
context: types<{ status: 'loaded'; user: User }>()
}
}
}
});
const machine = machineSetup.createFSM({
initial: 'loading',
context: { status: 'loading' },
states: {
loading: {
on: {
resolve: ({ event }) => ({
target: 'loaded',
context: { status: 'loaded' as const, user: event.user }
})
}
},
loaded: {}
}
});
const [state] = machine.transition(machine.initialState, {
type: 'resolve',
user: { id: '1' }
});
if (state.value === 'loaded') {
state.context.user.id; // typed as string
} else {
state.context.status; // typed as 'loading'
}Run as an actor
An FSM is actor logic. Pass it to createActor from xstate to run it as an
actor:
import { createActor } from 'xstate';
import { createFSM } from 'xstate/fsm';
const machine = createFSM({
initial: 'inactive',
states: {
inactive: { on: { toggle: 'active' } },
active: { on: { toggle: 'inactive' } }
}
});
const actor = createActor(machine).start();
actor.subscribe((state) => console.log(state.value));
actor.send({ type: 'toggle' }); // logs 'active'The pure transition(logic, snapshot, event) and initialTransition(logic)
functions from xstate also accept an FSM. actor.getPersistedSnapshot()
returns the FSM snapshot as is, and createActor(machine, { snapshot })
restores it.
xstate/fsm itself does not import the actor runtime. Importing createActor
from xstate adds the runtime to your bundle.
Behavior
createFSM is pure. It does not create or run actors:
machine.initialStateis the initial{ status: 'active', value, context }. FSM snapshots are always'active'. Theoutputanderrorfields of the snapshot type are alwaysundefinedand are not set on the object.machine.getInitialSnapshot()returnsmachine.initialState.machine.transition(state, event)returns[nextState, effects].effectsis always empty.- Missing transitions return the current state unchanged.
- A transition can be a state name,
{ target, context }, or a function that receives{ context, event }and returns{ target, context }. Returnundefinedfrom a function for no transition. - Context updates are shallow patches.
types<T>()adds types without runtime validation. Setup schema values are ignored by the FSM runtime; stateschemas.inputis accepted for upgrade compatibility but no input is consumed by this pure API.
The package intentionally has no actions, guards, effects, timers, child
actors, hierarchical or parallel states, or eventless transitions. Use
createMachine and createActor from xstate when the state machine needs
those statechart or runtime features.