Skip to content

Using XState machines ​

When an actor's lifecycle has real states and guarded transitions, back it with an XState machine. The machine constrains which transitions are legal; your onTransition handlers decide what happens when one fires.

1. Define the machine ​

ts
import { createMachine } from 'xstate';

export const compareMachine = createMachine({
  id: 'compare',
  initial: 'comparing',
  states: {
    comparing: { on: { SELECT_FORK: 'comparing', ACCEPT_FORK: 'resolved', SEND_TO_REVIEWER: 'inReview' } },
    inReview:  { on: { APPROVE: 'resolved' } },
    resolved:  { type: 'final' },
  },
});

2. Attach it and handle transitions ​

ts
import { defineBehavior } from '@actor-web/runtime';

const compare = defineBehavior<CompareEvent>()
  .withMachine(compareMachine)
  .onTransition({
    ACCEPT_FORK: ({ actor }) => ({ reply: actor.getSnapshot().value }),
    APPROVE:     ({ actor }) => ({ reply: actor.getSnapshot().value }),
  })
  .build();

The runtime checks the machine before running a handler: a message that isn't a legal transition from the current state is rejected, and the handler never runs.

Guards and rejection ​

Put guards in the machine. If SELECT_FORK isn't allowed in inReview, sending it there is a no-op rejection — you don't need defensive if checks in handlers.

When to prefer withFSM ​

If you only need transition constraints (no hierarchy, no XState actions), the lightweight withFSM constraint map avoids the XState dependency while keeping the same onTransition ergonomics.

Test it ​

Use @actor-web/testing to assert a machine has no unreachable states and to report transition coverage.