Covers XState v5 actor model, actor types, invocation, spawning, and communication. Use when choosing between promise/callback/observable/state-machine actors, implementing invoke vs spawn, designing parent-child communication, or managing actor lifecycle with input/output.
Covers XState v5 actor model, actor types, invocation, spawning, and communication. Use when choosing between promise/callback/observable/state-machine actors, implementing invoke vs spawn, designing parent-child communication, or managing actor lifecycle with input/output.
XState v5 Actors and Invocation
Actor Model
In XState, actors are independent entities that:
Have their own encapsulated internal state
Communicate via asynchronous message passing (events)
Process one message at a time (internal "mailbox" queue)
Cannot share or directly access another actor's state
Can create (spawn/invoke) new actors
Actor Logic Types
Type
Receive Events
Send Events
Spawn Actors
Input
Output
createMachine()
Yes
Yes
Yes
Yes
Yes
fromPromise()
No
Yes
No
Yes
Yes
fromCallback()
Yes
Yes
No
Yes
No
fromObservable()
No
Yes
No
Yes
No
fromEventObservable()
No
Yes
No
Yes
No
fromTransition()
Yes
Yes
No
Yes
No
Promise Actors
For async operations that resolve or reject:
import { fromPromise } from'xstate';
const fetchUser = fromPromise(async ({ input }: { input: { userId: string } }) => {
const res = awaitfetch(`/api/users/${input.userId}`);
if (!res.ok) thrownewError('Failed');
return res.json(); // This becomes event.output in onDone
});
Callback Actors
For bidirectional communication, event listeners, intervals:
import { fromCallback } from'xstate';
const keyListener = fromCallback(({ sendBack, receive, input }) => {
consthandler = (e: KeyboardEvent) => {
sendBack({ type: 'KEY_PRESS', key: e.key });
};
document.addEventListener('keydown', handler);
// Receive events from parentreceive((event) => {
if (event.type === 'PAUSE') { /* ... */ }
});
// Cleanup function — called when actor is stoppedreturn() =>document.removeEventListener('keydown', handler);
});
Observable Actors
For streams of values (requires RxJS or compatible):
invoke: {
src: 'logger',
systemId: 'logger', // Unique across the entire actor system
}
// Any actor in the system can address itactions: sendTo(({ system }) => system.get('logger'), { type: 'LOG' }),
Lifecycle
Invoked actors start on state entry, stop on state exit
Spawned actors start when spawned, survive state changes, stop when parent stops or stopChild() is called
If a state is entered and immediately exited (via always), invoked actors are NOT started
toPromise
Convert any actor to a Promise:
import { toPromise } from'xstate';
const actor = createActor(machine).start();
const output = awaittoPromise(actor);
// Resolves with actor's output when done, rejects on error
Anti-Patterns
Orphaned Spawned Refs
// BAD — spawned ref in context not cleaned upactions: stopChild('worker'),
// workerRef still in context, pointing to stopped actor!// GOOD — always clean upactions: [stopChild('worker'), assign({ workerRef: undefined })],
Using sendParent() (tight coupling)
// BAD — child is tightly coupled to parent's event typesimport { sendParent } from'xstate';
actions: sendParent({ type: 'DONE' }),
// GOOD — pass parent ref via inputcontext: ({ input }) => ({ parentRef: input.parentRef }),
actions: sendTo(({ context }) => context.parentRef, { type: 'DONE' }),
Async in Actions
// BAD — actions are NOT awaitedentry: async () => { awaitfetch('/api') },
// GOOD — use invoke for asyncinvoke: {
src: fromPromise(() =>fetch('/api')),
onDone: { /* ... */ },
onError: { /* ... */ },
}