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.
Using actors in Stately Studio​
Read about using invoked actors in Stately Studio.
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 one at a time.
- 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.
Creating 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. The types of actor logic you can create from XState are:
- State machine logic (
createMachine(...)
) - Promise logic (
fromPromise(...)
) - Transition logic (
fromTransition(...)
) - Observable logic (
fromObservable(...)
) - Event observable logic (
fromEventObservable(...)
) - Callback logic (
fromCallback(...)
)
In XState, actor logic is defined by an object containing methods like .transition(...)
, .getInitialState()
, .getSnapshot()
, 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 interpret(actorLogic, options?)
. The interpret(...)
function takes the following arguments:
actorLogic
: the actor logic to interpretoptions
(optional): interpreter options
When you interpret(...)
actor logic, 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 { interpret } from 'xstate';
import { someActorLogic } from './someActorLogic.ts';
const actor = interpret(someActorLogic);
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. Spawed 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()
.
An actor’s snapshot is not necessarily the same as its internal state; instead, it is a value the actor wants to share with subscribers. For example, a promise actor has an internal state consisting of data
, status
, and other internal properties, but only the data
is emitted as its snapshot.
import { fromPromise, interpret } from 'xstate';
const countLogic = fromPromise(async () => {
const count = await fetchCount();
return count;
});
const countActor = interpret(countLogic);
countActor.start();
countActor.getSnapshot(); // logs undefined
// After the promise resolves...
countActor.getSnapshot(); // logs 42
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(data) {
// ...
},
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 observers will automatically be unsubscribed.
You can initialize actor logic at a specific persisted internal state by passing the state in the second options
argument of interpret(logic, options)
. If the state is compatible with the actor logic, this will create an actor that will be started at that persisted state:
You can initialize actor logic at a specific persisted internal state by passing the state in the second options
argument of interpret(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.get('some-persisted-state'));
const actor = interpret(someLogic, {
state: persistedState,
});
// Actor will start at persisted state
actor.start();
See persistence for more details.
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:
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.
await waitFor(
countActor,
(snapshot) => {
return snapshot.count >= 100;
},
{
timeout: 10_000, // 10 seconds (10,000 milliseconds)
},
);
State machine actor logic​
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({
initial: 'inactive',
states: {
inactive: {},
active: {},
},
});
const toggleActor = interpret(toggleMachine);
toggleActor.subscribe((snapshot) => {
console.log(snapshot.value);
});
toggleActor.start();
// Logs 'inactive'
toggleActor.send({ type: 'toggle' });
// Logs 'active'
Promise actor logic​
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('...').then((data) => data.json());
});
const promiseActor = interpret(promiseLogic);
promiseActor.subscribe((snapshot) => {
console.log(snapshot);
});
promiseActor.start();
// Logs undefined
// After promise resolves
// Logs user: { name: ... }
Transition actors​
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 = interpret(transitionLogic);
transitionActor.subscribe((snapshot) => {
console.log(snapshot);
});
transitionActor.start();
// Logs { count: 0 }
transitionActor.send({ type: 'increment' });
// Logs { count: 1 }
Observable actors​
Observable actor logic is described by an observable stream of values. Actors created from observable logic (“observable actors”) can:
- Emit snapshots of its emitted value
Sending events to observable actors will have no effect.
const secondLogic = fromObservable(() => interval(1000));
const secondActor = interpret(secondLogic);
secondActor.start();
// At every second:
// Logs 0
// Logs 1
// Logs 2
// ...
Event observable actors​
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.
const mouseClickLogic = fromEventObservable(() =>
fromEventPattern(document.body, 'click'),
);
const canvasMachine = createMachine({
invoke: {
// Will send mouse click events to the canvas actor
src: mouseClickLogic,
},
});
const canvasActor = interpret(canvasMachine);
canvasActor.start();
Callback actors​
Callback actor logic is described by a callback function that receives a sendBack
function and a receive
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);
};
});
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:
function withLogging(actorLogic) {
const enhancedLogic = {
...actorLogic,
transition: (state, event, actorCtx) => {
console.log('State:', state);
return actorLogic.transition(state, event, actorCtx);
},
};
return enhancedLogic;
}
const loggingToggleLogic = withLogging(toggleLogic);
Custom actors​
Coming soon
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
// ...
}
TypeScript​
Coming soon
Cheatsheet​
Coming soon