| title | Compact finite state machines |
|---|---|
| description | Build tiny, typed, flat state machines with xstate/fsm. |
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.
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'
}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 restoreActor(machine, persisted) from
xstate/persist restores it.
xstate/fsm itself does not import the actor runtime. Importing createActor
from xstate adds the runtime to your bundle.
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. - Only own entries in
statesandondefine transitions. A state inherited from thestatesprototype has no outgoing transitions, like an undeclared state. Transition targets are not validated. - Context updates are shallow patches. A patch adds its own enumerable string and symbol keys even when a new value is
undefinedor matches an inherited value. If the target is unchanged, a patch whose keys already exist withObject.is-equal values keeps the current snapshot. 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.