This guide uses patterns found in a large kiosk application. That application has a root session router, checkout and menu routers, named actors, device callback actors, selector-heavy React views, delayed transitions, persisted snapshots, and exit animations.
| XState pattern | Effect Machine form |
|---|---|
setup().createMachine() |
Machine.make() fluent builder |
machine context |
fields on tagged machine states |
machine input |
initial: (input) => State.X(input) |
machine output |
.final(State.X, mapper) |
assign |
return a new state, often with State.X.with |
| guarded transition array | repeated .when() calls and an .on() fallback |
always |
.immediate |
| invoked promise actor | .task |
| invoked callback actor | .spawn with an Effect resource or Stream |
| root invoked child machine | self.spawn in .spawn or .background |
| machine-wide invoked listener | .background |
after |
.timeout |
| deferred event handling | .postpone |
sendTo |
child ActorRef.send or named system lookup |
waitFor |
actor.waitFor |
| actor registry | ActorSystemService |
useSelector |
ActorAtom.select plus a framework Atom hook |
| inspector action and guard logs | task, Effect, transition, and named guard inspection events |
Keep a root session machine for visible application phases. Spawn one child for the active phase. State scope stops the child when the route changes.
Do not compose interactive screens only with Effect.flatMap. An Effect sequence has no parent state for navigation, retained UI data, or cross-phase events. Use Effect.flatMap for autonomous jobs. Use a parent machine for interactive routing.
See actor-system.ts.
The kiosk application starts one callback actor per visible machine state. The callback observes actor snapshots. It removes the previous key listener. It then maps directional input to actor events.
In Effect Machine, model the device as a service with a Stream or scoped subscription. Consume it in .spawn(State.Visible, ...). The state scope owns listener removal. This removes the separate callback actor protocol.
Do not migrate actions: [] as a new action list.
- Context assignments become one returned tagged state.
- Async work that reports a result becomes a task.
- State entry subscriptions become state-owned Effects.
- Telemetry becomes an Effect service call.
- Work that must complete before publication becomes an Effectful transition.
If several updates must be atomic, compute one next state in one transition handler.
Map promise actors to .task. The task name appears in inspection. Map callback actors to .spawn or .background. Use Effect.acquireRelease, Effect.async, or Stream to express listener cleanup.
The machine type records all service requirements. A missing API, bridge, storage, or device service prevents actor startup at compile time.
Create one writable Actor Atom for the actor. Derive stable selections with ActorAtom.select. Select primitives or stable references when possible. Add an equality function for object projections.
Do not put component callbacks into machine state. Keep domain state in the actor. Keep view composition in the framework.
The kiosk application keeps temporary values because the external store publishes the new machine state before Motion removes the old screen.
Prefer a domain fix:
- Identify the old tree data that remains valid during exit.
- Carry that data into the next or final tagged state.
- Select the value from every state that renders it.
- Let the animation library own only mount retention.
Use a UI-local retained value only when the value is purely visual and has no domain meaning. Do not let that local value become a second machine context.
Use lifecycle.recovery and lifecycle.durability for actor state that must survive a new local actor. Use terminal state retention only for the existing actor reference. These are different lifetimes.
See Persistence and supervision.
- Define schema-first states and events.
- Move context fields into the state variants that own them.
- Port unconditional edges with
.on()and conditional edges with.when(). - Port
always, delays, and postponed events. - Port invoked promise work as tasks.
- Port callback actors as scoped Effect resources or Streams.
- Rebuild the root actor topology with
ActorSystemServiceand child actors. - Add Atom selectors and render-count tests.
- Add inspection, recovery, durability, and supervision.
- Remove temporary retained-value hooks after state variants retain required exit data.