Skip to content
Version: XState v5

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 from
  • options (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:

Actor logic capabilities​

Receive eventsSend eventsSpawn actorsInputOutput
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);
};
});