State machines
A state machine is a model that describes the behavior of something, for example an actor. Finite state machines describe how the state of an actor transitions to another state when an event occurs.
Benefits of state machines
State machines help build reliable and robust software. Read more about the benefits of state machines.
Creating a state machine
In XState, a state machine (referred to as a “machine”) is created using the createMachine(config)
function:
import { createMachine } from 'xstate';
const feedbackMachine = createMachine({
id: 'feedback',
initial: 'question',
states: {
question: {
on: {
'feedback.good': {
target: 'thanks',
},
},
},
thanks: {
// ...
},
// ...
},
});
In this example, the machine has two states: question
and thanks
. The question
state has a transition to the thanks
state when the feedback.good
event is sent to the machine:
const feedbackActor = createActor(feedbackMachine);
feedbackActor.subscribe((state) => {
console.log(state.value);
});
feedbackActor.start();
// logs 'question'
feedbackActor.send({ type: 'feedback.good' });
// logs 'thanks'
Creating actors from machines
A machine contains the logic of an actor. An actor is a running instance of the machine; in other words, it is the entity whose logic is described by the machine. Multiple actors can be created from the same machine, and each of those actors will exhibit the same behavior (reaction to received events), but they will be independent of each other and have their own states.
To create an actor, use the createActor(machine)
function:
import { createActor } from 'xstate';
const feedbackActor = createActor(feedbackMachine);
feedbackActor.subscribe((state) => {
console.log(state.value);
});
feedbackActor.start();
// logs 'question'
You can also create an actor from other types of logic, such as functions, promises, and observables.
Providing implementations
Machine implementations are the language-specific code that is executed but is not directly related to the state machine’s logic (states and transitions). This includes:
- Actions, which are fire-and-forget side-effects.
- Actors, which are entities that can communicate with the machine actor.
- Guards, which are conditions that determine whether a transition should be taken.
- Delays, which specify the time before a delayed transition is taken or a delayed event is sent.
The default implementations can be provided in a setup({...})
function when creating a machine, and then you can reference those implementations using JSON-serializable strings and/or objects, such as { type: 'doSomething' }
.
import { setup } from 'xstate';
const feedbackMachine = setup({
// Default implementations
actions: {
doSomething: () => {
console.log('Doing something!');
},
},
actors: {
/* ... */
},
guards: {
/* ... */
},
delays: {
/* ... */
},
}).createMachine({
entry: { type: 'doSomething' },
// ... rest of machine config
});
const feedbackActor = createActor(feedbackMachine);
feedbackActor.start();
// logs 'Doing something!'
You can override default implementations by providing implementations via machine.provide(...)
. This function will create a new machine with the same config, but with the provided implementations:
const customFeedbackMachine = feedbackMachine.provide({
actions: {
doSomething: () => {
console.log('Doing something else!');
},
},
});
const feedbackActor = createActor(customFeedbackMachine);
feedbackActor.start();
// logs 'Doing something else!'
Transitioning state
Since XState version 5.19.0
When you create a state machine actor, the next state is determined by the machine's current state and the event that is sent to the actor. However, you can also determine the next state and actions from the current state and event by using the pure transition(machine, state, event)
and initialTransition(machine)
functions:
import { initialTransition, transition } from 'xstate';
const machine = createMachine({
initial: 'pending',
states: {
pending: {
on: {
start: { target: 'started' },
},
},
started: {
entry: 'doSomething',
},
},
});
const [initialState, initialActions] = initialTransition(machine);
console.log(initialState.value);
// logs 'pending'
console.log(initialActions);
// logs []
const nextState = transition(machine, initialState, { type: 'start' });
console.log(nextState.value);
// logs 'started'
console.log(nextState.actions);
// logs [{ type: 'doSomething', … }]
Determining the next state
When you create a state machine actor, the next state is determined by the machine's current state and the event that is sent to the actor. If you want to determine the next state outside of the actor, you can use the getNextSnapshot(…)
function:
import { getNextSnapshot } from 'xstate';
import { feedbackMachine } from './feedbackMachine';
const nextSnapshot = getNextSnapshot(
feedbackMachine,
feedbackMachine.resolveState({ value: 'question' }),
{ type: 'feedback.good' },
);
console.log(nextSnapshot.value);
// logs 'thanks'
You can also determine the initial state of a machine using the getInitialSnapshot(…)
function:
import { getInitialSnapshot } from 'xstate';
import { feedbackMachine } from './feedbackMachine';
const initialSnapshot = getInitialSnapshot(
feedbackMachine,
// optional input
{ defaultRating: 3 },
);
console.log(initialSnapshot.value);
// logs 'question'
Specifying types
You can specify TypeScript types inside the machine setup using the .types
property:
import { setup } from 'xstate';
const feedbackMachine = setup({
types: {
context: {} as { feedback: string },
events: {} as { type: 'feedback.good' } | { type: 'feedback.bad' },
},
actions: {
logTelemetry: () => {
// TODO: implement
},
},
}).createMachine({
// ...
});
These types will be inferred throughout the machine config and in the created machine and actor so that methods such as machine.transition(...)
and actor.send(...)
will be type-safe.
Typegen
Typegen does not yet support XState v5. However, with the setup(...)
function and/or the .types
property explained above, you can provide strong typing for most (if not all) of your machine.
Read more about setting up state machines.
API
Coming soon…
Machines and TypeScript
The best way to provide strong typing for your machine is to use the setup(...)
function and/or the .types
property.
import { setup, fromPromise } from 'xstate';
const someAction = () => {
/* ... */
};
const someGuard = ({ context }) => context.count <= 10;
const someActor = fromPromise(async () => {
// ...
return 42;
});
const feedbackMachine = setup({
types: {
context: {} as { count: number },
events: {} as { type: 'increment' } | { type: 'decrement' },
},
actions: {
someAction,
},
guards: {
someGuard,
},
actors: {
someActor,
},
}).createMachine({
initial: 'counting',
states: {
counting: {
entry: { type: 'someAction' }, // strongly-typed
invoke: {
src: 'someActor', // strongly-typed
onDone: {
actions: ({ event }) => {
event.output; // strongly-typed as number
},
},
},
on: {
increment: {
guard: { type: 'someGuard' }, // strongly-typed
actions: assign({
count: ({ context }) => context.count + 1,
}),
},
},
},
},
});
Machine cheatsheet
Use our XState machine cheatsheet below to get started quickly.
Cheatsheet: create a machine
import { createMachine } from 'xstate';
const machine = createMachine({
initial: 'start',
states: {
start: {},
// ...
},
});
Cheatsheet: setup a machine with implementations
import { setup } from 'xstate';
const machine = setup({
actions: {
someAction: () => {
/* ... */
},
},
guards: {
someGuard: ({ context }) => context.count <= 10,
},
actors: {
someActor: fromPromise(async () => {
/* ... */
}),
},
delays: {
someDelay: () => 1000,
},
}).createMachine({
// ... Rest of machine config
});
Cheatsheet: provide implementations
import { createMachine } from 'xstate';
import { someMachine } from './someMachine';
const machineWithImpls = someMachine.provide({
actions: {
/* ... */
},
actors: {
/* ... */
},
guards: {
/* ... */
},
delays: {
/* ... */
},
});