# Invoking Services
Expressing the entire app's behavior in a single machine can quickly become complex and unwieldy. It is natural (and encouraged!) to use multiple machines that communicate with each other to express complex logic instead. This closely resembles the Actor model (opens new window), where each machine instance is considered an "actor" that can send and receive events (messages) to and from other "actors" (such as Promises or other machines) and react to them.
For machines to communicate with each other, the parent machine invokes a child machine and listens to events sent from the child machine via sendParent(...)
, or waits for the child machine to reach its final state, which will then cause the onDone
transition to be taken.
You can invoke:
- Promises, which will take the
onDone
transition onresolve
, or theonError
transition onreject
- Callbacks, which can send events to and receive events from the parent machine
- Observables, which can send events to the parent machine, as well as a signal when it is completed
- Machines, which can also send/receive events, and also notify the parent machine when it reaches its final state
# The invoke
Property
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 service to invoke, which can be:- a machine
- a function that returns a
Promise
- a function that returns a "callback handler"
- a function that returns an observable
- a string, which refers to any of the 4 listed options defined in this machine's
options.services
- an invoke source object 4.12, which contains the source string in
{ type: src }
, as well as any other metadata.
id
- the unique identifier for the invoked serviceonDone
- (optional) the transition to be taken when:- the child machine reaches its final state, or
- the invoked promise resolves, or
- the invoked observable completes
onError
- (optional) the transition to be taken when the invoked service encounters an execution error.autoForward
- (optional)true
if all events sent to this machine should also be sent (or forwarded) to the invoked child (false
by default)- ⚠️ Avoid setting
autoForward
totrue
, as blindly forwarding all events may lead to unexpected behavior and/or infinite loops. Always prefer to explicitly send events, and/or use theforward(...)
action creator to directly forward an event to an invoked child. (works currently for machines only! ⚠️)
- ⚠️ Avoid setting
data
- (optional, used only when invoking machines) an object that maps properties of the child machine's context to a function that returns the corresponding value from the parent machine'scontext
.
WARNING
Don't get the onDone
property on a state confused with invoke.onDone
- they are similar transitions, but refer to different things.
- The
onDone
property on a state node refers to the compound state node reaching a final state. - The
invoke.onDone
property refers to the invocation (invoke.src
) being done.
// ...
loading: {
invoke: {
src: someSrc,
onDone: {/* ... */} // refers to `someSrc` being done
},
initial: 'loadFoo',
states: {
loadFoo: {/* ... */},
loadBar: {/* ... */},
loadingComplete: { type: 'final' }
},
onDone: 'loaded' // refers to 'loading.loadingComplete' being reached
}
// ...
# Invoking Promises
Since every promise can be modeled as a state machine, XState can invoke promises as-is. Promises can either:
resolve()
, which will take theonDone
transitionreject()
(or throw an error), which will take theonError
transition
If the state where the invoked promise is active is exited before the promise settles, the result of the promise is discarded.
// Function that returns a promise
// This promise might resolve with, e.g.,
// { name: 'David', location: 'Florida' }
const fetchUser = (userId) =>
fetch(`url/to/user/${userId}`).then((response) => response.json());
const userMachine = createMachine({
id: 'user',
initial: 'idle',
context: {
userId: 42,
user: undefined,
error: undefined
},
states: {
idle: {
on: {
FETCH: { target: 'loading' }
}
},
loading: {
invoke: {
id: 'getUser',
src: (context, event) => fetchUser(context.userId),
onDone: {
target: 'success',
actions: assign({ user: (context, event) => event.data })
},
onError: {
target: 'failure',
actions: assign({ error: (context, event) => event.data })
}
}
},
success: {},
failure: {
on: {
RETRY: { target: 'loading' }
}
}
}
});
The resolved data is placed into a 'done.invoke.<id>'
event, under the data
property, e.g.:
{
type: 'done.invoke.getUser',
data: {
name: 'David',
location: 'Florida'
}
}
# Promise Rejection
If a Promise rejects, the onError
transition will be taken with a { type: 'error.platform' }
event. The error data is available on the event's data
property:
const search = (context, event) => new Promise((resolve, reject) => {
if (!event.query.length) {
return reject('No query specified');
// or:
// throw new Error('No query specified');
}
return resolve(getSearchResults(event.query));
});
// ...
const searchMachine = createMachine({
id: 'search',
initial: 'idle',
context: {
results: undefined,
errorMessage: undefined,
},
states: {
idle: {
on: {
SEARCH: { target: 'searching' }
}
},
searching: {
invoke: {
id: 'search'
src: search,
onError: {
target: 'failure',
actions: assign({
errorMessage: (context, event) => {
// event is:
// { type: 'error.platform', data: 'No query specified' }
return event.data;
}
})
},
onDone: {
target: 'success',
actions: assign({ results: (_, event) => event.data })
}
}
},
success: {},
failure: {}
}
});
WARNING
If the onError
transition is missing and the Promise is rejected, the error will be ignored unless you have specified strict mode for the machine. Strict mode will stop the machine and throw an error in this case.
# Invoking Callbacks
Streams of events sent to the parent machine can be modeled via a callback handler, which is a function that takes in two arguments:
callback
- called with the event to be sentonReceive
- called with a listener that listens to events from the parent
The (optional) return value should be a function that performs cleanup (i.e., unsubscribing, preventing memory leaks, etc.) on the invoked service when the current state is exited. Callbacks cannot use async/await
syntax because it automatically wraps the return value in a Promise
.
// ...
counting: {
invoke: {
id: 'incInterval',
src: (context, event) => (callback, onReceive) => {
// This will send the 'INC' event to the parent every second
const id = setInterval(() => callback('INC'), 1000);
// Perform cleanup
return () => clearInterval(id);
}
},
on: {
INC: { actions: assign({ counter: context => context.counter + 1 }) }
}
}
// ...
# Listening to Parent Events
Invoked callback handlers are also given a second argument, onReceive
, which registers listeners for events sent to the callback handler from the parent. This allows for parent-child communication between the parent machine and the invoked callback service.
For example, the parent machine sends the child 'ponger'
service a 'PING'
event. The child service can listen for that event using onReceive(listener)
, and send a 'PONG'
event back to the parent in response:
const pingPongMachine = createMachine({
id: 'pinger',
initial: 'active',
states: {
active: {
invoke: {
id: 'ponger',
src: (context, event) => (callback, onReceive) => {
// Whenever parent sends 'PING',
// send parent 'PONG' event
onReceive((e) => {
if (e.type === 'PING') {
callback('PONG');
}
});
}
},
entry: send({ type: 'PING' }, { to: 'ponger' }),
on: {
PONG: { target: 'done' }
}
},
done: {
type: 'final'
}
}
});
interpret(pingPongMachine)
.onDone(() => done())
.start();
# Invoking Observables 4.6
Observables (opens new window) are streams of values emitted over time. Think of them as an array/collection whose values are emitted asynchronously, instead of all at once. There are many implementations of observables in JavaScript; the most popular one is RxJS (opens new window).
Observables can be invoked, which is expected to send events (strings or objects) to the parent machine, yet not receive events (uni-directional). An observable invocation is a function that takes context
and event
as arguments and returns an observable stream of events. The observable is unsubscribed when the state where it is invoked is exited.
import { createMachine, interpret } from 'xstate';
import { interval } from 'rxjs';
import { map, take } from 'rxjs/operators';
const intervalMachine = createMachine({
id: 'interval',
initial: 'counting',
context: { myInterval: 1000 },
states: {
counting: {
invoke: {
src: (context, event) =>
interval(context.myInterval).pipe(
map((value) => ({ type: 'COUNT', value })),
take(5)
),
onDone: 'finished'
},
on: {
COUNT: { actions: 'notifyCount' },
CANCEL: { target: 'finished' }
}
},
finished: {
type: 'final'
}
}
});
The above intervalMachine
will receive the events from interval(...)
mapped to event objects, until the observable is "completed" (done emitting values). If the "CANCEL"
event happens, the observable will be disposed (.unsubscribe()
will be called internally).
TIP
Observables don't necessarily need to be created for every invocation. A "hot observable" can be referenced instead:
import { fromEvent } from 'rxjs';
const mouseMove$ = fromEvent(document.body, 'mousemove');
const mouseMachine = createMachine({
id: 'mouse',
// ...
invoke: {
src: (context, event) => mouseMove$
},
on: {
mousemove: {
/* ... */
}
}
});
# Invoking Machines
Machines communicate hierarchically, and invoked machines can communicate:
- Parent-to-child via the
send(EVENT, { to: 'someChildId' })
action - Child-to-parent via the
sendParent(EVENT)
action.
If the state where the machine is invoked is exited, the machine is stopped.
import { createMachine, interpret, send, sendParent } from 'xstate';
// Invoked child machine
const minuteMachine = createMachine({
id: 'timer',
initial: 'active',
states: {
active: {
after: {
60000: { target: 'finished' }
}
},
finished: { type: 'final' }
}
});
const parentMachine = createMachine({
id: 'parent',
initial: 'pending',
states: {
pending: {
invoke: {
src: minuteMachine,
// The onDone transition will be taken when the
// minuteMachine has reached its top-level final state.
onDone: 'timesUp'
}
},
timesUp: {
type: 'final'
}
}
});
const service = interpret(parentMachine)
.onTransition((state) => console.log(state.value))
.start();
// => 'pending'
// ... after 1 minute
// => 'timesUp'
# Invoking with Context
Child machines can be invoked with context
that is derived from the parent machine's context
with the data
property. For example, the parentMachine
below will invoke a new timerMachine
service with initial context of { duration: 3000 }
:
const timerMachine = createMachine({
id: 'timer',
context: {
duration: 1000 // default duration
}
/* ... */
});
const parentMachine = createMachine({
id: 'parent',
initial: 'active',
context: {
customDuration: 3000
},
states: {
active: {
invoke: {
id: 'timer',
src: timerMachine,
// Deriving child context from parent context
data: {
duration: (context, event) => context.customDuration
}
}
}
}
});
Just like assign(...)
, child context can be mapped as an object (preferred) or a function:
// Object (per-property):
data: {
duration: (context, event) => context.customDuration,
foo: (context, event) => event.value,
bar: 'static value'
}
// Function (aggregate), equivalent to above:
data: (context, event) => ({
duration: context.customDuration,
foo: event.value,
bar: 'static value'
})
WARNING
The data
replaces the default context
defined on the machine; it is not merged. This behavior will change in the next major version.
# Done Data
When a child machine reaches its top-level final state, it can send data in the "done" event (e.g., { type: 'done.invoke.someId', data: ... }
). This "done data" is specified on the final state's data
property:
const secretMachine = createMachine({
id: 'secret',
initial: 'wait',
context: {
secret: '42'
},
states: {
wait: {
after: {
1000: { target: 'reveal' }
}
},
reveal: {
type: 'final',
data: {
secret: (context, event) => context.secret
}
}
}
});
const parentMachine = createMachine({
id: 'parent',
initial: 'pending',
context: {
revealedSecret: undefined
},
states: {
pending: {
invoke: {
id: 'secret',
src: secretMachine,
onDone: {
target: 'success',
actions: assign({
revealedSecret: (context, event) => {
// event is:
// { type: 'done.invoke.secret', data: { secret: '42' } }
return event.data.secret;
}
})
}
}
},
success: {
type: 'final'
}
}
});
const service = interpret(parentMachine)
.onTransition((state) => console.log(state.context))
.start();
// => { revealedSecret: undefined }
// ...
// => { revealedSecret: '42' }
# Sending Events
- To send from a child machine to a parent machine, use
sendParent(event)
(takes the same arguments assend(...)
) - To send from a parent machine to a child machine, use
send(event, { to: <child ID> })
WARNING
The send(...)
and sendParent(...)
action creators do not imperatively send
events to machines. They are pure functions that return an action object
describing what is to be sent, e.g., { type: 'xstate.send', event: ... }
. An
interpreter will read these objects and then send them.
Here is an example of two machines, pingMachine
and pongMachine
, communicating with each other:
import { createMachine, interpret, send, sendParent } from 'xstate';
// Parent machine
const pingMachine = createMachine({
id: 'ping',
initial: 'active',
states: {
active: {
invoke: {
id: 'pong',
src: pongMachine
},
// Sends 'PING' event to child machine with ID 'pong'
entry: send({ type: 'PING' }, { to: 'pong' }),
on: {
PONG: {
actions: send({ type: 'PING' }, { to: 'pong', delay: 1000 })
}
}
}
}
});
// Invoked child machine
const pongMachine = createMachine({
id: 'pong',
initial: 'active',
states: {
active: {
on: {
PING: {
// Sends 'PONG' event to parent machine
actions: sendParent('PONG', {
delay: 1000
})
}
}
}
}
});
const service = interpret(pingMachine).start();
// => 'ping'
// ...
// => 'pong'
// ..
// => 'ping'
// ...
// => 'pong'
// ...
# Sending Responses 4.7+
An invoked service (or spawned actor) can respond to another service/actor; i.e., it can send an event in response to an event sent by another service/actor. This is done with the respond(...)
action creator.
For 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 { createMachine, send, actions } from 'xstate';
const { respond } = actions;
const authServerMachine = createMachine({
id: 'server',
initial: 'waitingForCode',
states: {
waitingForCode: {
on: {
CODE: {
actions: respond('TOKEN', { delay: 1000 })
}
}
}
}
});
const authClientMachine = createMachine({
id: 'client',
initial: 'idle',
states: {
idle: {
on: {
AUTH: { target: 'authorizing' }
}
},
authorizing: {
invoke: {
id: 'auth-server',
src: authServerMachine
},
entry: send({ type: 'CODE' }, { to: 'auth-server' }),
on: {
TOKEN: { target: 'authorized' }
}
},
authorized: {
type: 'final'
}
}
});
This specific example can use sendParent(...)
for the same effect; the difference is that respond(...)
will send an event back to the received event's origin, which might not necessarily be the parent machine.
# Multiple Services
You can invoke multiple services by specifying each in an array:
// ...
invoke: [
{ id: 'service1', src: 'someService' },
{ id: 'service2', src: 'someService' },
{ id: 'logService', src: 'logService' }
],
// ...
Each invocation will create a new instance of that service, so even if the src
of multiple services are the same (e.g., 'someService'
above), multiple instances of 'someService'
will be invoked.
# Configuring Services
The invocation sources (services) can be configured similar to how actions, guards, etc. are configured -- by specifying the src
as a string and defining them in the services
property of the Machine options:
const fetchUser = // (same as the above example)
const userMachine = createMachine(
{
id: 'user',
// ...
states: {
// ...
loading: {
invoke: {
src: 'getUser',
// ...
}
},
// ...
}
},
{
services: {
getUser: (context, event) => fetchUser(context.user.id)
}
);
The invoke src
can also be specified as an object 4.12 that describes the invoke source with its type
and other related metadata. This can be read from the services
option in the meta.src
argument:
const machine = createMachine(
{
initial: 'searching',
states: {
searching: {
invoke: {
src: {
type: 'search',
endpoint: 'example.com'
}
// ...
}
// ...
}
}
},
{
services: {
search: (context, event, { src }) => {
console.log(src);
// => { endpoint: 'example.com' }
}
}
}
);
# Testing
By specifying services as strings above, "mocking" services can be done by specifying an alternative implementation with .withConfig()
:
import { interpret } from 'xstate';
import { assert } from 'chai';
import { userMachine } from '../path/to/userMachine';
const mockFetchUser = async (userId) => {
// Mock however you want, but ensure that the same
// behavior and response format is used
return { name: 'Test', location: 'Anywhere' };
};
const testUserMachine = userMachine.withConfig({
services: {
getUser: (context, event) => mockFetchUser(context.id)
}
});
describe('userMachine', () => {
it('should go to the "success" state when a user is found', (done) => {
interpret(testUserMachine)
.onTransition((state) => {
if (state.matches('success')) {
assert.deepEqual(state.context.user, {
name: 'Test',
location: 'Anywhere'
});
done();
}
})
.start();
});
});
# Referencing Services 4.7+
Services (and actors, which are spawned services) can be referenced directly on the state object from the .children
property. The state.children
object is a mapping of service IDs (keys) to those service instances (values):
const machine = createMachine({
// ...
invoke: [
{ id: 'notifier', src: createNotifier },
{ id: 'logger', src: createLogger }
]
// ...
});
const service = interpret(machine)
.onTransition((state) => {
state.children.notifier; // service from createNotifier()
state.children.logger; // service from createLogger()
})
.start();
When JSON serialized, the state.children
object is a mapping of service IDs (keys) to objects containing meta data about that service.
# Quick Reference
The invoke
property
const machine = createMachine({
// ...
states: {
someState: {
invoke: {
// The `src` property can be:
// - a string
// - a machine
// - a function that returns...
src: (context, event) => {
// - a promise
// - a callback handler
// - an observable
},
id: 'some-id',
// (optional) forward machine events to invoked service (currently for machines only!)
autoForward: true,
// (optional) the transition when the invoked promise/observable/machine is done
onDone: { target: /* ... */ },
// (optional) the transition when an error from the invoked service occurs
onError: { target: /* ... */ }
}
}
}
});
Invoking Promises
// Function that returns a promise
const getDataFromAPI = () => fetch(/* ... */)
.then(data => data.json());
// ...
{
invoke: (context, event) => getDataFromAPI,
// resolved promise
onDone: {
target: 'success',
// resolved promise data is on event.data property
actions: (context, event) => console.log(event.data)
},
// rejected promise
onError: {
target: 'failure',
// rejected promise data is on event.data property
actions: (context, event) => console.log(event.data)
}
}
// ...
Invoking Callbacks
// ...
{
invoke: (context, event) => (callback, onReceive) => {
// Send event back to parent
callback({ type: 'SOME_EVENT' });
// Receive events from parent
onReceive(event => {
if (event.type === 'DO_SOMETHING') {
// ...
}
});
},
// Error from callback
onError: {
target: 'failure',
// Error data is on event.data property
actions: (context, event) => console.log(event.data)
}
},
on: {
SOME_EVENT: { /* ... */ }
}
Invoking Observables
import { map } from 'rxjs/operators';
// ...
{
invoke: {
src: (context, event) => createSomeObservable(/* ... */).pipe(
map(value => ({ type: 'SOME_EVENT', value }))
),
onDone: 'finished'
}
},
on: {
SOME_EVENT: /* ... */
}
// ...
Invoking Machines
const someMachine = createMachine({ /* ... */ });
// ...
{
invoke: {
src: someMachine,
onDone: {
target: 'finished',
actions: (context, event) => {
// Child machine's done data (.data property of its final state)
console.log(event.data);
}
}
}
}
// ...
← Activities Actors →