Actors
When you run a state machine, it becomes an actor: a running process that can receive events, send events and change its behavior based on the events it receives, which can cause effects outside of the actor.
In state machines, actors can be invoked or spawned. These are essentially the same, with the only difference being how the actor’s lifecycle is controlled.
- An invoked actor is started when its parent machine enters the state it is invoked in, and stopped when that state is exited.
- A spawned actor is started in a transition and stopped either with a
stop(...)
action or when its parent machine is stopped.
Actor model​
In the actor model, actors are objects that can communicate with each other. They are independent “live” entities that communicate via asynchronous message passing. In XState, these messages are referred to as events.
- An actor has its own internal, encapsulated state that can only be updated by the actor itself. An actor may choose to update its internal state in response to a message it receives, but it cannot be updated by any other entity.
- Actors communicate with other actors by sending and receiving events asynchronously.
- Actors process one message at a time. They have an internal “mailbox” that acts like an event queue, processing events sequentially.
- Internal actor state is not shared between actors. The only way for an actor to share any part of its internal state is by:
- Sending events to other actors
- Or emitting snapshots, which can be considered implicit events sent to subscribers.
- Actors can create (spawn/invoke) new actors.
Read more about the Actor model
Actor logic​
Actor logic is the actor’s logical “model” (brain, blueprint, DNA, etc.) It describes how the actor should change behavior when receiving an event. You can create actor logic using actor logic creators.
In XState, actor logic is defined by an object implementing the ActorLogic
interface, containing methods like .transition(...)
, .getInitialSnapshot()
, .getPersistedSnapshot()
, and more. This object tells an interpreter how to update an actor’s internal state when it receives an event and which effects to execute (if any).
Creating actors​
You can create an actor, which is a “live” instance of some actor logic, via createActor(actorLogic, options?)
. The createActor(...)
function takes the following arguments:
actorLogic
: the actor logic to create an actor fromoptions
(optional): actor options
When you create an actor from actor logic via createActor(actorLogic)
, you implicitly create an actor system where the created actor is the root actor. Any actors spawned from this root actor and its descendants are part of that actor system. The actor must be started by calling actor.start()
, which will also start the actor system:
import { createActor } from 'xstate';
import { someActorLogic } from './someActorLogic.ts';
const actor = createActor(someActorLogic);
actor.subscribe((snapshot) => {
console.log(snapshot);
});
actor.start();
// Now the actor can receive events
actor.send({ type: 'someEvent' });
You can stop root actors by calling actor.stop()
, which will also stop the actor system and all actors in that system:
// Stops the root actor, actor system, and actors in the system
actor.stop();
Invoking and spawning actors​
An invoked actor represents a state-based actor, so it is stopped when the invoking state is exited. Invoked actors are used for a finite/known number of actors.
A spawned actor represents multiple entities that can be started at any time and stopped at any time. Spawned actors are action-based and used for a dynamic or unknown number of actors.
An example of the difference between invoking and spawning actors could occur in a todo app. When loading todos, a loadTodos
actor would be an invoked actor; it represents a single state-based task. In comparison, each of the todos can themselves be spawned actors, and there can be a dynamic number of these actors.
Actor snapshots​
When an actor receives an event, its internal state may change. An actor may emit a snapshot when a state transition occurs. You can read an actor’s snapshot synchronously via actor.getSnapshot()
, or you can subscribe to snapshots via actor.subscribe(observer)
.
import { fromPromise, createActor } from 'xstate';
async function fetchCount() {
return Promise.resolve(42);
}
const countLogic = fromPromise(async () => {
const count = await fetchCount();
return count;
});
const countActor = createActor(countLogic);
countActor.start();
countActor.getSnapshot(); // logs undefined
// After the promise resolves...
countActor.getSnapshot();
// => {
// output: 42,
// status: 'done',
// ...
// }
Subscriptions​
You can subscribe to an actor’s snapshot values via actor.subscribe(observer)
. The observer will receive the actor’s snapshot value when it is emitted. The observer can be:
- A plain function that receives the latest snapshot, or
- An observer object whose
.next(snapshot)
method receives the latest snapshot
// Observer as a plain function
const subscription = actor.subscribe((snapshot) => {
console.log(snapshot);
});
// Observer as an object
const subscription = actor.subscribe({
next(snapshot) {
console.log(snapshot);
},
error(err) {
// ...
},
complete() {
// ...
},
});
The return value of actor.subscribe(observer)
is a subscription object that has an .unsubscribe()
method. You can call subscription.unsubscribe()
to unsubscribe the observer:
const subscription = actor.subscribe((snapshot) => {
/* ... */
});
// Unsubscribe the observer
subscription.unsubscribe();
When the actor is stopped, all of its observers will automatically be unsubscribed.
You can initialize actor logic at a specific persisted snapshot (state) by passing the state in the second options
argument of createActor(logic, options)
. If the state is compatible with the actor logic, this will create an actor that will be started at that persisted state:
const persistedState = JSON.parse(localStorage.getItem('some-persisted-state'));
const actor = createActor(someLogic, {
snapshot: persistedState,
});
actor.subscribe(() => {
localStorage.setItem(
'some-persisted-state',
JSON.stringify(actor.getPersistedSnapshot()),
);
});
// Actor will start at persisted state
actor.start();
See persistence for more details.
waitFor
​
You can wait for an actor’s snapshot to satisfy a predicate using the waitFor(actor, predicate, options?)
helper function. The waitFor(...)
function returns a promise that is:
- Resolved when the emitted snapshot satisfies the
predicate
function - Resolved immediately if the current snapshot already satisfies the
predicate
function - Rejected if an error is thrown or the
options.timeout
value is elapsed.
import { waitFor } from 'xstate';
import { countActor } from './countActor.ts';
const snapshot = await waitFor(
countActor,
(snapshot) => {
return snapshot.context.count >= 100;
},
{
timeout: 10_000, // 10 seconds (10,000 milliseconds)
},
);
console.log(snapshot.output);
// => 100
Error handling​
You can subscribe to errors thrown by an actor using the error
callback in the observer object passed to actor.subscribe()
. This allows you to handle errors emitted by the actor logic.
import { createActor } from 'xstate';
import { someMachine } from './someMachine';
const actor = createActor(someMachine);
actor.subscribe({
next: (snapshot) => {
// ...
},
error: (err) => {
// Handle the error here
console.error(err);
},
});
actor.start();
Actor logic creators​
The types of actor logic you can create from XState are:
- State machine logic (
createMachine(...)
) - Promise logic (
fromPromise(...)
) - Transition function logic (
fromTransition(...)
) - Observable logic (
fromObservable(...)
) - Event observable logic (
fromEventObservable(...)
) - Callback logic (
fromCallback(...)
)
Actor logic capabilities​
Receive events | Send events | Spawn actors | Input | Output | |
---|---|---|---|---|---|
State machine actors | âś… | âś… | âś… | âś… | âś… |
Promise actors | ❌ | ✅ | ❌ | ✅ | ✅ |
Transition actors | ✅ | ✅ | ❌ | ✅ | ❌ |
Observable actors | ❌ | ✅ | ❌ | ✅ | ❌ |
Callback actors | ✅ | ✅ | ❌ | ✅ | ❌ |
State machine logic (createMachine(...)
)​
You can describe actor logic as a state machine. Actors created from state machine actor logic can:
- Receive events
- Send events to other actors
- Invoke/spawn child actors
- Emit snapshots of its state
- Output a value when the machine reaches its top-level final state
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: {},
active: {},
},
});
const toggleActor = createActor(toggleMachine);
toggleActor.subscribe((snapshot) => {
// snapshot is the machine's state
console.log('state', snapshot.value);
console.log('context', snapshot.context);
});
toggleActor.start();
// Logs 'inactive'
toggleActor.send({ type: 'toggle' });
// Logs 'active'
Learn more about state machine actors.
Promise logic (fromPromise(...)
)​
Promise actor logic is described by an async process that resolves or rejects after some time. Actors created from promise logic (“promise actors”) can:
- Emit the resolved value of the promise
- Output the resolved value of the promise
Sending events to promise actors will have no effect.
const promiseLogic = fromPromise(() => {
return fetch('https://example.com/...').then((data) => data.json());
});
const promiseActor = createActor(promiseLogic);
promiseActor.subscribe((snapshot) => {
console.log(snapshot);
});
promiseActor.start();
// => {
// output: undefined,
// status: 'active'
// ...
// }
// After promise resolves
// => {
// output: { ... },
// status: 'done',
// ...
// }
Learn more about promise actors.
Transition function logic (fromTransition(...)
)​
Transition actor logic is described by a transition function, similar to a reducer. Transition functions take the current state
and received event
object as arguments, and return the next state. Actors created from transition logic (“transition actors”) can:
- Receive events
- Emit snapshots of its state
const transitionLogic = fromTransition(
(state, event) => {
if (event.type === 'increment') {
return {
...state,
count: state.count + 1,
};
}
return state;
},
{ count: 0 },
);
const transitionActor = createActor(transitionLogic);
transitionActor.subscribe((snapshot) => {
console.log(snapshot);
});
transitionActor.start();
// => {
// status: 'active',
// context: { count: 0 },
// ...
// }
transitionActor.send({ type: 'increment' });
// => {
// status: 'active',
// context: { count: 1 },
// ...
// }
Learn more about transition actors.
Observable logic (fromObservable(...)
)​
Observable actor logic is described by an observable stream of values. Actors created from observable logic (“observable actors”) can:
- Emit snapshots of the observable’s emitted value
Sending events to observable actors will have no effect.
import { interval } from 'rxjs';
const secondLogic = fromObservable(() => interval(1000));
const secondActor = createActor(secondLogic);
secondActor.subscribe((snapshot) => {
console.log(snapshot.context);
});
secondActor.start();
// At every second:
// Logs 0
// Logs 1
// Logs 2
// ...
Learn more about observable actors.
Event observable logic (fromEventObservable(...)
)​
Event observable actor logic is described by an observable stream of event objects. Actors created from event observable logic (“event observable actors”) can:
- Implicitly send events to its parent actor
- Emit snapshots of its emitted event objects
Sending events to event observable actors will have no effect.
import { setup, fromEventObservable } from 'xstate';
import { fromEvent } from 'rxjs';
const mouseClickLogic = fromEventObservable(
() => fromEvent(document.body, 'click') as Subscribable<EventObject>,
);
const canvasMachine = setup({
actors: {
mouseClickLogic,
},
}).createMachine({
invoke: {
// Will send mouse click events to the canvas actor
src: 'mouseClickLogic',
},
});
const canvasActor = createActor(canvasMachine);
canvasActor.start();
Learn more about observable actors.
Callback logic (fromCallback(...)
)​
Callback actor logic is described by a callback function that receives a single object argument that includes a sendBack(event)
function and a receive(event => ...)
function. Actors created from callback logic (“callback actors”) can:
- Receive events via the
receive
function - Send events to the parent actor via the
sendBack
function
const callbackLogic = fromCallback(({ sendBack, receive }) => {
let lockStatus = 'unlocked';
const handler = (event) => {
if (lockStatus === 'locked') {
return;
}
sendBack(event);
};
receive((event) => {
if (event.type === 'lock') {
lockStatus = 'locked';
} else if (event.type === 'unlock') {
lockStatus = 'unlocked';
}
});
document.body.addEventListener('click', handler);
return () => {
document.body.removeEventListener('click', handler);
};
});
Callback actors are a bit different from other actors in that they do not do the following:
- Do not work with
onDone
- Do not produce a snapshot using
.getSnapshot()
- Do not emit values when used with
.subscribe()
- Can not be stopped with
.stop()
You may choose to use sendBack
to report caught errors to the parent actor. This is especially helpful for handling promise rejections within a callback function, which will not be caught by onError
.
Callback functions cannot be async
functions. But it is possible to execute a Promise within a callback function.
import { setup, fromCallback } from 'xstate';
const someCallback = fromCallback(({ sendBack }) => {
somePromise()
.then((data) => sendBack({ type: 'done', data }))
.catch((error) => sendBack({ type: 'error', data: error }));
return () => {
/* cleanup function */
};
});
const machine = setup({
actors: {
someCallback,
},
}).createMachine({
initial: 'running',
states: {
running: {
invoke: {
src: 'someCallback',
},
on: {
error: {
actions: ({ event }) => console.error(event.data),
},
},
},
},
});
Learn more about callback actors.
Actors as promises​
You can create a promise from any actor by using the toPromise(actor)
function. The promise will resolve with the actor snapshot's .output
when the actor is done (snapshot.status === 'done'
) or reject with the actor snapshot's .error
when the actor is errored (snapshot.status === 'error'
).
import { createMachine, createActor, toPromise } from 'xstate';
const machine = createMachine({
// ...
states: {
// ...
done: { type: 'final' },
},
output: {
count: 42,
},
});
const actor = createActor(machine);
actor.start();
// Creates a promise that resolves with the actor's output
// or rejects with the actor's error
const output = await toPromise(actor);
console.log(output);
// => { count: 42 }
If the actor is already done, the promise will resolve with the actor's snapshot.output
immediately. If the actor is already errored, the promise will reject with the actor's snapshot.error
immediately.
Higher-level actor logic​
Higher-level actor logic enhances existing actor logic with additional functionality. For example, you can create actor logic that logs or persists actor state:
import { fromTransition, type AnyActorLogic } from 'xstate';
const toggleLogic = fromTransition((state, event) => {
if (event.type === 'toggle') {
return state === 'paused' ? 'playing' : 'paused';
}
return state;
}, 'paused');
function withLogging<T extends AnyActorLogic>(actorLogic: T) {
const enhancedLogic = {
...actorLogic,
transition: (state, event, actorCtx) => {
console.log('State:', state);
return actorLogic.transition(state, event, actorCtx);
},
} satisfies T;
return enhancedLogic;
}
const loggingToggleLogic = withLogging(toggleLogic);
Custom actor logic​
Custom actor logic can be defined with an object that implements the ActorLogic
interface.
For example, here is a custom actor logic object with a transition
function that operates as a simple reducer:
import { createActor, EventObject, ActorLogic, Snapshot } from 'xstate';
const countLogic: ActorLogic<
Snapshot<undefined> & { context: number },
EventObject
> = {
transition: (state, event) => {
if (event.type === 'INC') {
return {
...state,
context: state.context + 1,
};
} else if (event.type === 'DEC') {
return {
...state,
context: state.context - 1,
};
}
return state;
},
getInitialSnapshot: () => ({
status: 'active',
output: undefined,
error: undefined,
context: 0,
}),
getPersistedSnapshot: (s) => s,
};
const actor = createActor(countLogic);
actor.subscribe((state) => {
console.log(state.context);
});
actor.start(); // => 0
actor.send({ type: 'INC' }); // => 1
actor.send({ type: 'INC' }); // => 2
For further examples, see implementations of ActorLogic
in the source code, like the fromTransition
actor logic creator, or the examples in the tests.
Empty actors​
Actor that does nothing and only has a single emitted snapshot: undefined
In XState, an empty actor is an actor that does nothing and only has a single emitted snapshot: undefined
.
This is useful for testing, such as stubbing out an actor that is not yet implemented. It can also be useful in framework integrations, such as @xstate/react
, where an actor may not be available yet:
import { createEmptyActor, AnyActorRef } from 'xstate';
import { useSelector } from '@xstate/react';
const emptyActor = createEmptyActor();
function Component(props: { actor?: AnyActorRef }) {
const data = useSelector(
props.actor ?? emptyActor,
(snapshot) => snapshot.context.data,
);
// data is `undefined` if `props.actor` is undefined
// Otherwise, it is the data from the actor
// ...
}
Actors and TypeScript​
You can strongly type the actors
of your machine in the types.actors
property of the machine config.
const fetcher = fromPromise(
async ({ input }: { input: { userId: string } }) => {
const user = await fetchUser(input.userId);
return user;
},
);
const machine = setup({
types: {
children: {} as {
fetch1: 'fetcher';
fetch2: 'fetcher';
}
}
actors: { fetcher }
}).createMachine({
invoke: {
src: 'fetchData', // strongly typed
id: 'fetch2', // strongly typed
onDone: {
actions: ({ event }) => {
event.output; // strongly typed as { result: string }
},
},
input: { userId: '42' }, // strongly typed
},
});
Testing​
The general strategy for testing actors is to send events and assert that the actor reaches an expected state, which can be observed either by:
- Subscribing to its emitted snapshots via
actor.subscribe(...)
- Or reading the latest snapshot via
actor.getSnapshot()
.
test('some actor', async () => {
const actor = createActor(
fromTransition(
(state, event) => {
if (event.type === 'inc') {
return { count: state.count + 1 };
}
return state;
},
{ count: 0 },
),
);
// Start the actor
actor.start();
// Send event(s)
actor.send({ type: 'inc' });
actor.send({ type: 'inc' });
actor.send({ type: 'inc' });
// Assert the expected result
expect(actor.getSnapshot().context).toEqual({ count: 3 });
});
Actors cheatsheet​
Cheatsheet: create an actor​
import { createActor } from 'xstate';
import { someActorLogic } from './someActorLogic.ts';
// Create an actor from the actor logic
const actor = createActor(someActorLogic);
// Subscrube to an actor’s snapshot values and log them
actor.subscribe((snapshot) => {
console.log(snapshot);
});
// Start the actor system
actor.start();
// Now the actor can receive events
actor.send({ type: 'someEvent' });
// Stops the root actor, actor system, and actors in the system
actor.stop();
Cheatsheet: state machine logic​
import { createMachine, createActor } from 'xstate';
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: {},
active: {},
},
});
const toggleActor = createActor(toggleMachine);
toggleActor.subscribe((snapshot) => {
// snapshot is the machine’s state
console.log('state', snapshot.value);
console.log('context', snapshot.context);
});
toggleActor.start();
// Logs 'inactive'
toggleActor.send({ type: 'toggle' });
// Logs 'active'
Cheatsheet: promise logic​
import { fromPromise, createActor } from 'xstate';
const promiseLogic = fromPromise(() => {
return fetch('https://example.com/...').then((data) => data.json());
});
const promiseActor = createActor(promiseLogic);
promiseActor.subscribe((snapshot) => {
console.log(snapshot);
});
promiseActor.start();
Cheatsheet: transition function logic​
import { fromTransition, createActor } from 'xstate';
const transitionLogic = fromTransition(
(state, event) => {
if (event.type === 'increment') {
return {
...state,
count: state.count + 1,
};
}
return state;
},
{ count: 0 },
);
const transitionActor = createActor(transitionLogic);
transitionActor.subscribe((snapshot) => {
console.log(snapshot);
});
transitionActor.start();
// => {
// status: 'active',
// context: { count: 0 },
// ...
// }
transitionActor.send({ type: 'increment' });
// => {
// status: 'active',
// context: { count: 1 },
// ...
// }
Cheatsheet: observable logic​
import { fromObservable, createActor } from 'xstate';
import { interval } from 'rxjs';
const secondLogic = fromObservable(() => interval(1000));
const secondActor = createActor(secondLogic);
secondActor.subscribe((snapshot) => {
console.log(snapshot.context);
});
secondActor.start();
// At every second:
// Logs 0
// Logs 1
// Logs 2
// ...
Cheatsheet: event observable logic​
import { setup, fromEventObservable, createActor } from 'xstate';
import { fromEvent } from 'rxjs';
const mouseClickLogic = fromEventObservable(
() => fromEvent(document.body, 'click') as Subscribable<EventObject>,
);
const canvasMachine = setup({
actors: {
mouseClickLogic,
},
}).createMachine({
invoke: {
// Will send mouse click events to the canvas actor
src: 'mouseClickLogic',
},
});
const canvasActor = createActor(canvasMachine);
canvasActor.start();
Cheatsheet: callback logic​
import { fromCallback, createActor } from 'xstate';
const callbackLogic = fromCallback(({ sendBack, receive }) => {
let lockStatus = 'unlocked';
const handler = (event) => {
if (lockStatus === 'locked') {
return;
}
sendBack(event);
};
receive((event) => {
if (event.type === 'lock') {
lockStatus = 'locked';
} else if (event.type === 'unlock') {
lockStatus = 'unlocked';
}
});
document.body.addEventListener('click', handler);
return () => {
document.body.removeEventListener('click', handler);
};
});