Skip to content
Version: XState v5

Invoke

State machines can "invoke" one or many actors within a given state. The invoked actor will start when the state is entered, and stop when the state is exited. Any XState actor can be invoked, including simple Promise-based actors, or even complex machine-based actors.

Invoking an actor is useful for managing synchronous or asynchronous work that the state machine needs to orchestrate and communicate with at a high level, but doesn't need to know about in detail.

Actors can be invoked within any state except for the top-level final state. In the following example, the loading state invokes a Promise-based actor:

import { createMachine, createActor, fromPromise, assign } from 'xstate';

const fetchUser = (userId: string) =>
fetch(`https://example.com/${userId}`).then((response) => response.text());

const userMachine = createMachine({
types: {} as {
context: {
userId: string;
user: object | undefined;
error: unknown;
};
},
id: 'user',
initial: 'idle',
context: {
userId: '42',
user: undefined,
error: undefined,
},
states: {
idle: {
on: {
FETCH: { target: 'loading' },
},
},
loading: {
invoke: {
id: 'getUser',
src: fromPromise(({ input }) => fetchUser(input.userId)),
input: ({ context: { userId } }) => ({ userId }),
onDone: {
target: 'success',
actions: assign({ user: ({ event }) => event.output }),
},
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => event.error }),
},
},
},
success: {},
failure: {
on: {
RETRY: { target: 'loading' },
},
},
},
});

Actors can also be invoked on the root of the machine, and they will be active for the lifetime of their parent machine actor:

import { fromEventObservable, fromEvent } from 'rxjs';
const interactiveMachine = createMachine({
invoke: {
src: fromEventObservable(
() => fromEvent(document.body, 'click') as Subscribable<EventObject>,
),
},
on: {
click: {
actions: ({ event }) => console.log(event),
},
},
});

And invoke can be an array, to invoke multiple actors:

const vitalsWorkflow = createMachine({
states: {
CheckVitals: {
invoke: [
{ src: 'checkTirePressure' },
{ src: 'checkOilPressure' },
{ src: 'checkCoolantLevel' },
{ src: 'checkBattery' },
],
},
},
});

For further examples, see:

How are Actors Different From Actions?​

Actions are β€œfire-and-forget”; as soon as their execution starts, the state machine running the actions forgets about them. If you specify an action as async, the action won’t be awaited before moving to the next state. Remember: transitions are always zero-time (states transition synchronously).

Invoked actors can do asynchronous work and communicate with their parent machine actor. They they can send and receive events. Invoked machine actors can even invoke or spawn their own child actors.

Unlike actions, errors thrown by invoked actors can be handled directly:

invoke: {
src: 'fetchUser',
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => event.error })
}
}

Whereas errors thrown by actions can only be handled globally by a subscriber of their parent state machine:

actor.subscribe({
error: (err) => {
console.error(err);
},
});

Lifecycle​

Invoked actors have a lifecycle that is managed by the state they are invoked in. They are created and started when the state is entered, and stopped when the state is exited.

If a state is entered and then immediately exited, e.g. due to an eventless ("always") transition, then no actors will be invoked on that state.

Re-Entering​

By default, when a state machine transitions from a parent state to the same parent state or a descendent (child or deeper), it will not re-enter the parent state. Because the transition is not re-entering, the parent state's existing invoked actors will not be stopped and new invoked actors will not be started.

However, if you want a transition to re-enter the parent state, set the transition's reenter property to true. Transitions that re-enter the state will stop existing invoked actors and start new invoked actors.

Read more about re-entering states.

The invoke Property API​

An invocation is defined in a state node's configuration with the invoke property, whose value is an object that contains:

  • src - The source of the actor logic to invoke when creating the actor, or a string referring to actor logic defined in the machine's provided implementation.
  • id - A string identifying the actor, unique within its parent machine.
  • input - The input to pass to the actor.
  • onDone - Transition that occurs when the actor is complete.
  • onError - Transition that occurs when the actor throws an error.
  • onSnapshot - Transition that occurs when the actor emits a new value.
  • systemId - A string identifing the actor, unique system-wide.

Source​

The src represents the actor logic the machine should use when creating the actor. There are several actor logic creators available in XState:

The invoke src can be inline or provided.

Inline src​

Either directly inline:

invoke: {
src: fromPromise(…)
}

Or from some logic in the same scope as the machine:

const logic = fromPromise(…)
const machine = createMachine({
// …
invoke: {
src: logic
}
});

Provided src​

The src can be provided in the machine implementation and referenced using a string or an object.

const machine = createMachine({
// …
invoke: {
src: 'workflow', // string reference
},
});

const actor = createActor(
machine.provide({
actors: {
workflow: fromPromise(/* ... */), // provided
},
}),
);

onDone​

  • Transitions when invoked actor is complete
  • Event object output property is provided with actor's output data
  • Not available for callback actors

The onError transition can be an object:

{
invoke: {
src: 'fetchItem',
onDone: {
target: 'success'
actions: ({ event }) => {
console.log(event.output);
}
},
onError: {
target: 'error',
actions: ({ event }) => {
console.error(event.error);
}
}
}
}

Or, for simplicity, target-only transitions can be strings:

{
invoke: {
src: 'fetchItem',
onDone: 'success'
onError: 'error'
}
}

onError​

  • Transitions when invoked actor throws an error, or (for Promise-based actors) when the promise rejects
  • Event object error property is provided with actor’s error data
invoke: {
src: 'getUser',
onError: {
target: 'failure',
actions: ({ event }) => {
console.error(event.error);
}
}
}

Or, for simplicity, target-only transitions can be strings:

{
invoke: {
src: 'getUser',
onError: 'failure'
}
}

onSnapshot​

  • Transitions when invoked actor emits a new snapshot
  • Event gets data with actor's snapshot
  • Not available for callback actors
invoke: {
src: 'getUser',
onSnapshot: {
actions: ({ event }) => console.log(event.data)
}
}

Read more about actor snapshots.

Input​

To define input to an invoked actor, use input.

The input property can be a static input value, or a function that returns the input value. The function will be passed an object that contains the current context and event.

Input from a static value​

invoke: {
src: 'liveFeedback',
input: {
domain: 'stately.ai'
}
}

Input from a function​

invoke: {
src: fromPromise(({ input: { endpoint, userId } }) => {
return fetch(`${endpoint}/${userId}`).then((res) => res.json());
}),
input: ({ context, event }) => ({
endpoint: context.endpoint,
userId: event.userId
})
}

See Input for more.

Invoking Promises​

The most common type of actors you’ll invoke are promise actors. Promise actors allow you to await the result of a promise before deciding what to do next.

XState can invoke Promises as actors using the fromPromise actor logic creator. Promises can:

  • resolve(), which will take the onDone transition
  • reject() (or throw an error), which will take the onError transition

If the state where the invoked promise is active is exited before the promise settles, the result of the promise is discarded.

import { createMachine, createActor, fromPromise, assign } from 'xstate';

// Function that returns a Promise
// which resolves with some useful value
// e.g.: { name: 'David', location: 'Florida' }
const fetchUser = (userId: string) =>
fetch(`/api/users/${userId}`).then((response) => response.json());

const userMachine = createMachine({
types: {} as {
context: {
userId: string;
user: object | undefined;
error: unknown;
};
},
id: 'user',
initial: 'idle',
context: {
userId: '42',
user: undefined,
error: undefined,
},
states: {
idle: {
on: {
FETCH: { target: 'loading' },
},
},
loading: {
invoke: {
id: 'getUser',
src: fromPromise(({ input }) => fetchUser(input.userId)),
input: ({ context: { userId } }) => ({ userId }),
onDone: {
target: 'success',
actions: assign({ user: ({ event }) => event.output }),
},
onError: {
target: 'failure',
actions: assign({ error: ({ event }) => event.error }),
},
},
},
success: {},
failure: {
on: {
RETRY: { target: 'loading' },
},
},
},
});

The resolved output is placed into a 'xstate.done.actor.<id>' event, under the output property, e.g.:

{
type: 'xstate.done.actor.getUser',
output: {
name: 'David',
location: 'Florida'
}
}

Promise Rejection​

If a Promise rejects, the onError transition will be taken with a { type: 'xstate.error.actor.<id>' } event. The error data is available on the event's error property:

import { createMachine, createActor, fromPromise, assign } from 'xstate';

const search = (query: string) =>
new Promise((resolve, reject) => {
if (!query.length) {
return reject('No query specified');
// or:
// throw new Error('No query specified');
}

return resolve(getSearchResults(query));
});

// ...
const searchMachine = createMachine({
types: {} as {
context: {
results: object | undefined;
errorMessage: unknown;
};
},
id: 'search',
initial: 'idle',
context: {
results: undefined,
errorMessage: undefined,
},
states: {
idle: {
on: {
SEARCH: { target: 'searching' },
},
},
searching: {
invoke: {
id: 'search',
src: fromPromise(({ input: { query } }) => search(query)),
input: ({ event }) => ({ query: event.query }),
onError: {
target: 'failure',
actions: assign({
errorMessage: ({ context, event }) => {
// event is:
// { type: 'xstate.error.actor.<id>', error: 'No query specified' }
return event.error;
},
}),
},
onDone: {
target: 'success',
actions: assign({ results: ({ event }) => event.output }),
},
},
},
success: {},
failure: {},
},
});

If the onError transition is missing, and the Promise is rejected, the error will throw. However, you can handle all thrown errors for an actor by subscribing an observer object with an error function:

actor.subscribe({
error: (err) => { ... }
})

Invoking Callbacks​

invoke: {
src: fromCallback(/* … */);
}

Coming soon

Invoking Observables​

invoke: {
src: fromObservable(/* … */);
}

Coming soon

Invoking Event Observables​

invoke: {
src: fromEventObservable(/* … */);
}

Coming soon

Invoking Transitions​

invoke: {
src: fromTransition(/* … */);
}

Coming soon

Invoking Machines​

invoke: {
src: createMachine(/* … */);
}

Coming soon

Sending Responses​

An invoked actor (or spawned actor) can respond to another actor; i.e., it can send an event in response to an event sent by another actor. To do so, provide a reference to the sending actor as a custom property on the event object being sent. In the following example, we use event.sender, but any name works.

// Parent
actions: sendTo('childActor', ({ self }) => ({
type: 'ping',
sender: self,
}));

// Child
actions: sendTo(
({ event }) => event.sender,
{ type: 'pong' },
);

In the following example, the 'client' machine below sends the 'CODE' event to the invoked 'auth-server' service, which then responds with a 'TOKEN' event after 1 second.

import { createActor, createMachine, sendTo } from 'xstate';

const authServerMachine = createMachine({
id: 'server',
initial: 'waitingForCode',
states: {
waitingForCode: {
on: {
CODE: {
actions: sendTo(
({ event }) => event.sender,
{ type: 'TOKEN' },
{ delay: 1000 },
),
},
},
},
},
});

const authClientMachine = createMachine({
id: 'client',
initial: 'idle',
states: {
idle: {
on: {
AUTH: { target: 'authorizing' },
},
},
authorizing: {
invoke: {
id: 'auth-server',
src: authServerMachine,
},
entry: sendTo('auth-server', ({ self }) => ({
type: 'CODE',
sender: self,
})),
on: {
TOKEN: { target: 'authorized' },
},
},
authorized: {
type: 'final',
},
},
});

Note that by default sendTo will send events anonymously, in which case the reciever will not know the source of the event.

Multiple Actors​

You can invoke multiple actors by specifying each in an array:

invoke: [
{ id: 'actor1', src: 'someActor' },
{ id: 'actor2', src: 'someActor' },
{ id: 'logActor', src: 'logActor' },
];

Each invocation will create a new instance of that actor, so even if the src of multiple actors are the same (e.g., someActor above), multiple instances of someActor will be invoked.

Testing​

Coming soon

Referencing Invoked Actors​

Actors can be read on snapshot.children.<actorId>. The returned value is an ActorRef object, with properties like:

  • id - the ID of the actor
  • send()
  • getSnapshot()
actor.subscribe({
next(snapshot) {
console.log(Object.keys(snapshot.children));
},
});

snapshot.children is a key-value object where the keys are the actor ID and the value is the ActorRef.

TypeScript​

Coming soon

Cheatsheet​

Coming soon