# XState v5 Actors Reference ## Actor Model Fundamentals Actors are independent entities that: - Maintain private, encapsulated state - Communicate via asynchronous message passing (events) - Process messages sequentially from an internal mailbox - Can create new child actors - Can only expose internal state through events or snapshots ## Actor Types Overview | Actor Type | Receives Events | Sends Events | Spawns Actors | Has Output | Use Case | | ------------- | --------------- | ------------ | ------------- | ---------- | ------------------- | | State Machine | Yes | Yes | Yes | Yes | Complex state logic | | Promise | No | Yes | No | Yes | Async operations | | Transition | Yes | Yes | No | No | Pure reducers | | Callback | Yes | Yes | No | No | Imperative logic | | Observable | No | Yes | No | No | Streams | ## Creating and Managing Actors ### Basic Actor Creation ```typescript import { createActor } from 'xstate'; const actor = createActor(someLogic, { id: 'myActor', input: { initialData: 'value' }, snapshot: previousSnapshot, // For restoration systemId: 'mySystem', logger: console.log, }); // Lifecycle actor.start(); // Must call to begin processing actor.stop(); // Stops actor and all children ``` ### Actor Subscription ```typescript // Simple observer function const subscription = actor.subscribe((snapshot) => { console.log('State:', snapshot.value); console.log('Context:', snapshot.context); }); // Full observer object actor.subscribe({ next(snapshot) { console.log('New snapshot:', snapshot); }, error(err) { console.error('Actor error:', err); }, complete() { console.log('Actor completed'); }, }); // Cleanup subscription.unsubscribe(); ``` ### Snapshot Access ```typescript // Synchronous snapshot access const currentSnapshot = actor.getSnapshot(); // Persistable snapshot (serializable) const persistedSnapshot = actor.getPersistedSnapshot(); localStorage.setItem('actor-state', JSON.stringify(persistedSnapshot)); ``` ## State Machine Actors The most powerful actor type, implementing full statechart semantics. ```typescript import { createMachine, createActor } from 'xstate'; const toggleMachine = createMachine({ id: 'toggle', initial: 'inactive', context: { count: 0 }, states: { inactive: { on: { TOGGLE: { target: 'active', actions: assign({ count: ({ context }) => context.count + 1 }), }, }, }, active: { on: { TOGGLE: 'inactive' }, }, }, }); const actor = createActor(toggleMachine); actor.start(); actor.send({ type: 'TOGGLE' }); ``` ## Promise Actors Handle single async operations that resolve or reject. ```typescript import { fromPromise, createActor } from 'xstate'; // Basic promise actor const fetchLogic = fromPromise(async () => { const response = await fetch('/api/data'); return response.json(); }); // With input parameter const fetchWithInput = fromPromise(async ({ input }) => { const response = await fetch(`/api/data/${input.id}`); return response.json(); }); // With abort signal const cancellableFetch = fromPromise(async ({ input, signal }) => { const response = await fetch('/api/data', { signal }); return response.json(); }); // Usage const actor = createActor(fetchWithInput, { input: { id: '123' }, }); actor.subscribe({ next: (snapshot) => { if (snapshot.status === 'done') { console.log('Result:', snapshot.output); } }, error: (err) => console.error('Failed:', err), }); actor.start(); ``` ## Transition Actors Pure reducers that transform state based on events. ```typescript import { fromTransition, createActor } from 'xstate'; const counterLogic = fromTransition( (state, event) => { switch (event.type) { case 'INC': return { ...state, count: state.count + 1 }; case 'DEC': return { ...state, count: state.count - 1 }; case 'RESET': return { count: 0 }; default: return state; } }, { count: 0 }, // Initial state ); const actor = createActor(counterLogic); actor.start(); actor.send({ type: 'INC' }); console.log(actor.getSnapshot().context.count); // 1 ``` ## Callback Actors Imperative actors for complex side effects and cleanup. ```typescript import { fromCallback, createActor } from 'xstate'; const intervalLogic = fromCallback(({ sendBack, receive, input }) => { // Setup console.log('Starting with input:', input); const interval = setInterval(() => { sendBack({ type: 'TICK', timestamp: Date.now() }); }, 1000); // Event handler receive((event) => { if (event.type === 'STOP_TICKING') { clearInterval(interval); } }); // Cleanup function (called on stop) return () => { clearInterval(interval); console.log('Cleaned up'); }; }); const actor = createActor(intervalLogic, { input: { message: 'Timer started' }, }); actor.subscribe((snapshot) => { console.log('Snapshot:', snapshot); }); actor.start(); // Later: actor.send({ type: 'STOP_TICKING' }); // Or: actor.stop(); ``` ## Observable Actors Integration with reactive streams (RxJS, etc.). ```typescript import { fromObservable, createActor } from 'xstate'; import { interval } from 'rxjs'; import { map } from 'rxjs/operators'; const timerLogic = fromObservable(() => interval(1000).pipe(map((i) => ({ type: 'TICK', count: i }))), ); const actor = createActor(timerLogic); actor.subscribe((snapshot) => { console.log('Event from observable:', snapshot.context); }); actor.start(); ``` ## Invoking vs Spawning Actors ### Invoked Actors (State-based) Tied to state lifecycle - start on entry, stop on exit. ```typescript const machine = createMachine({ states: { loading: { invoke: { id: 'fetcher', src: fromPromise(async () => { const res = await fetch('/api'); return res.json(); }), input: ({ context }) => ({ url: context.apiUrl }), onDone: { target: 'success', actions: assign({ data: ({ event }) => event.output, }), }, onError: { target: 'failure', actions: assign({ error: ({ event }) => event.error, }), }, onSnapshot: { actions: ({ event }) => { console.log('Invoked actor snapshot:', event.snapshot); }, }, }, }, success: {}, failure: {}, }, }); ``` ### Spawned Actors (Action-based) Created dynamically, managed manually. ```typescript import { spawn, stopChild } from 'xstate'; const parentMachine = createMachine({ context: { todos: [], todoActors: {}, }, on: { ADD_TODO: { actions: assign({ todoActors: ({ context, spawn }) => { const id = Math.random().toString(); const todoActor = spawn(todoMachine, { id: `todo-${id}`, input: { text: event.text }, syncSnapshot: true, // Sync snapshots with parent }); return { ...context.todoActors, [id]: todoActor, }; }, }), }, REMOVE_TODO: { actions: [ stopChild(({ context, event }) => `todo-${event.id}`), assign({ todoActors: ({ context, event }) => { const { [event.id]: removed, ...rest } = context.todoActors; return rest; }, }), ], }, }, }); ``` ## Actor Communication ### Sending Between Actors ```typescript import { sendTo, sendParent } from 'xstate'; const childMachine = createMachine({ entry: [ // Send to parent sendParent({ type: 'CHILD_READY' }), // Or use emit (preferred in v5) emit({ type: 'CHILD_READY' }), ], on: { PING: { actions: emit({ type: 'PONG' }), }, }, }); const parentMachine = createMachine({ invoke: { id: 'child', src: childMachine, }, on: { SEND_TO_CHILD: { actions: sendTo('child', { type: 'PING' }), }, PONG: { actions: log('Received pong from child'), }, }, }); ``` ### Actor System ```typescript // Access system from any actor const actor = createActor(machine); actor.start(); // Get actor from system const childActor = actor.system.get('childId'); // Inspect all actors in system actor.system._set.forEach((actor, id) => { console.log(`Actor ${id}:`, actor.getSnapshot()); }); ``` ## Waiting for Actors ```typescript import { waitFor } from 'xstate'; // Wait for condition const doneSnapshot = await waitFor( actor, (snapshot) => snapshot.status === 'done', { timeout: 5000 }, // Optional timeout ); // Convert to promise import { toPromise } from 'xstate'; const result = await toPromise(actor); // Resolves with output when done // Rejects with error when failed ``` ## Error Handling ### Promise Actor Errors ```typescript const errorProneLogic = fromPromise(async () => { throw new Error('Something went wrong'); }); const machine = createMachine({ invoke: { src: errorProneLogic, onError: { target: 'errorState', actions: ({ event }) => { console.error('Error:', event.error); }, }, }, }); ``` ### Callback Actor Error Reporting ```typescript const callbackWithErrors = fromCallback(({ sendBack }) => { try { // risky operation } catch (error) { sendBack({ type: 'error.platform', error }); } }); ``` ## Actor Persistence ```typescript // Save actor state const actor = createActor(machine); actor.subscribe(() => { const snapshot = actor.getPersistedSnapshot(); localStorage.setItem('actor-state', JSON.stringify(snapshot)); }); actor.start(); // Restore actor state const savedState = localStorage.getItem('actor-state'); if (savedState) { const restoredActor = createActor(machine, { snapshot: JSON.parse(savedState), }); restoredActor.start(); } ``` ## Best Practices ### 1. Choose the Right Actor Type - **State machines**: Complex logic with multiple states - **Promises**: Single async operations - **Transitions**: Pure state reducers - **Callbacks**: Imperative code with cleanup - **Observables**: Stream integrations ### 2. Actor Lifecycle Management ```typescript // Always subscribe before starting const subscription = actor.subscribe(observer); actor.start(); // Clean up properly subscription.unsubscribe(); actor.stop(); ``` ### 3. Error Boundaries ```typescript // Always handle errors in parent const parentMachine = createMachine({ invoke: { src: childLogic, onError: { actions: [ log('Child failed'), assign({ error: ({ event }) => event.error }), ], }, }, }); ``` ### 4. Input Validation ```typescript const actorWithInput = fromPromise(async ({ input }) => { // Validate input if (!input?.id) { throw new Error('ID required'); } return fetchData(input.id); }); ``` ### 5. Sync vs Async Snapshots ```typescript // Spawned actors can sync snapshots with parent spawn(childMachine, { id: 'child', syncSnapshot: true, // Parent re-renders on child changes }); ``` ## Testing Actors ```typescript import { createActor } from 'xstate'; import { expect, test } from 'vitest'; test('actor processes events correctly', async () => { const actor = createActor(machine); const states = []; actor.subscribe((snapshot) => { states.push(snapshot.value); }); actor.start(); actor.send({ type: 'EVENT1' }); actor.send({ type: 'EVENT2' }); expect(states).toEqual(['initial', 'state1', 'state2']); }); test('promise actor resolves', async () => { const actor = createActor(promiseLogic); actor.start(); const result = await toPromise(actor); expect(result).toEqual({ data: 'expected' }); }); ```