Events and transitions
A transition is a change from one finite state to another, triggered by an event.
An event is a signal, trigger, or message that causes a transition. When an actor receives an event, its machine will determine if there are any enabled transitions for that event in the current state. If enabled transitions exist, the machine will take them and execute their actions.
Transitions are “deterministic”; each combination of state and event always points to the same next state. When a state machine receives an event, only the active finite states are checked to see if any of them have a transition for that event. Those transitions are called enabled transitions. If there is an enabled transition, the state machine will execute the transition's actions, and then transition to the target state.
Transitions are represented by on:
in a state:
import { createMachine } from 'xstate';
const feedbackMachine = createMachine({
id: 'feedback',
initial: 'question',
states: {
question: {
on: {
'feedback.good': {
target: 'thanks'
}
}
},
thanks: {}
},
});
Event objects
In XState, events are represented by event objects with a type
property and optional payload:
- The
type
property is a string that represents the event type. - The payload is an object that contains additional data about the event.
feedbackActor.send({
// The event type
type: 'feedback.update',
// Additional payload
feedback: 'This is great!',
rating: 5,
});
Selecting transitions
Transitions are selected by checking the deepest child states first. If the transition is enabled (i.e. if its guard passes), it will be taken. If not, the parent state will be checked, and so on.
- Start on the deepest active state nodes (aka atomic state nodes)
- If the transition is enabled (no
guard
or itsguard
evaluates totrue
), select it. - If no transition is enabled, go up to the parent state node and repeat step 1.
- Finally, if no transitions are enabled, no transitions will be taken, and the state will not change.
Self-transitions
A state can transition to itself. This is known as a self-transition, and is useful for changing context and/or executing actions without changing the finite state. You can also use self-transitions to restart a state.
Root self-transitions:
import { createMachine, assign } from 'xstate';
const machine = createMachine({
context: { count: 0 },
on: {
someEvent: {
// No target
actions: assign({
count: ({context}) => context.count + 1,
})
}
}
});
Self-transitions on states:
import { createMachine, assign } from 'xstate';
const machine = createMachine({
context: { count: 0 },
initial: 'inactive',
states: {
inactive: {
on: { activate: { target: 'active' } }
},
active: {
on: {
someEvent: {
// No target
actions: assign({
count: ({context}) => context.count + 1,
})
}
}
}
}
});
Transitions between states
Usually, transitions are between two sibling states. These transitions are defined by setting the target
as the sibling state key.
const feedbackMachine = createMachine({
// ...
states: {
form: {
on: {
submit: {
// Target is the key of the sibling state
target: 'submitting',
},
},
},
submitting: {
// ...
},
},
});
Parent to child transitions
When a state machine actor receives an event, it will first check the deepest (atomic) state to see if there is any enabled transition. If not, the parent state is checked, and so on, until the state machine reaches the root state.
When you want an event to transition to a state regardless of which sibling state is active, a useful pattern is to transition from the parent state to the child state.
For example, the below state machine will transition to the colorMode.system
state on the mode.reset
event regardless of which state it is currently in.
import { createMachine } from 'xstate';
const machine = createMachine({
id: 'colorMode',
initial: 'system',
states: {
system: {},
auto: {},
light: {
on: {
'mode.toggle': { target: 'dark' }
}
},
dark: {
on: {
'mode.toggle': { target: 'light' }
}
}
},
on: {
'mode.reset': {
target: '.system'
}
}
});
Re-entering
By default, when a state machine transitions from some state to the same state or from a parent state to a descendent (child, grandchild, etc.) of that parent state, it will not re-enter the state; that is, it will not execute the exit
and entry
actions of the parent state. It will not stop existing invoked actors or start new invoked actors.
This can be changed with the transition reenter
property: if you want the parent state to be re-entered, you can set reenter: true
. This will cause the state to re-enter when transitioning to itself or descendent states, executing the exit
and entry
actions of the state. It will stop existing invoked actors, and start new invoked actors.
Self-transitions with reenter: true
:
import { createMachine } from 'xstate';
const machine = createMachine({
initial: 'someState',
states: {
someState: {
entry: () => console.log('someState entered'),
exit: () => console.log('someState exited'),
on: {
'event.normal': {
target: 'someState', // or no target
},
'event.thatReenters': {
target: 'someState', // or no target
reenter: true,
}
}
}
}
});
const actor = createActor(machine);
actor.start();
actor.send({ type: 'event.normal' });
// Does not log anything
actor.send({ type: 'event.thatReenters' });
// Logs:
// "someState exited"
// "someState entered"
Parent-child (or descendent) transitions with reenter: true
:
const machine = createMachine({
initial: 'parentState',
states: {
parentState: {
entry: () => console.log('parentState entered'),
exit: () => console.log('parentState exited'),
on: {
'event.normal': {
target: '.someChildState'
},
'event.thatReenters': {
target: '.otherChildState',
reenter: true
}
},
initial: 'someChildState',
states: {
someChildState: {
entry: () => console.log('someChildState entered'),
exit: () => console.log('someChildState exited')
},
otherChildState: {
entry: () => console.log('otherChildState entered'),
exit: () => console.log('otherChildState exited')
}
}
}
}
});
const actor1 = createActor(machine);
actor1.start();
actor1.send({ type: 'event.normal' });
// Logs:
// "someChildState exited"
// "someChildState entered"
const actor2 = createActor(machine);
actor2.start();
console.log('---');
actor2.send({ type: 'event.thatReenters' });
// Logs:
// "someChildState exited"
// "parentState exited"
// "parentState entered"
// "otherChildState entered"
Transitions to any state
Sibling descendent states: { target: 'sibling.child.grandchild' }
Parent to descendent states: { target: '.child.grandchild' }
State to any state: { target: '#specificState' }
Forbidden transitions
{ on: { forbidden: {} } }
- Different than omitting the transition; transition selection algorithm will stop looking
- Same as
{ on: { forbidden: { target: undefined } } }
Wildcard transitions
A wildcard transition is a transition that will match any event. The event descriptor (key of the on: {...}
object) is defined using the *
wildcard character as the event type:
import { createMachine } from 'xstate';
const feedbackMachine = createMachine({
initial: 'asleep',
states: {
asleep: {
on: {
// This transition will match any event
'*': { target: 'awake' },
},
},
awake: {},
},
});
Wildcard transitions are useful for:
- handling events that are not handled by any other transition.
- as a “catch-all” transition that handles any event in a state.
A wildcard transition has the least priority; it will only be taken if no other transitions are enabled.
Partial wildcard transitions
A partial wildcard transition is a transition that matches any event that starts with a specific prefix. The event descriptor is defined by using the wildcard character (*
) after a dot (.
) as the event type:
import { createMachine } from 'xstate';
const feedbackMachine = createMachine({
initial: 'prompt',
states: {
prompt: {
on: {
// This will match the 'feedback' event as well as
// any event that starts with 'feedback.', e.g.:
// 'feedback.good', 'feedback.bad', etc.
'feedback.*': { target: 'form' },
},
},
form: {},
// ...
},
});
The wildcard character (*
) can only be used in the suffix of an event descriptor, following a dot (.
):
Valid wildcard examples
- ✅
mouse.*
: matchesmouse
,mouse.click
,mouse.move
, etc. - ✅
mouse.click.*
: matchesmouse.click
,mouse.click.left
,mouse.click.right
, etc.
Invalid wildcard
- 🚫
: invalid; does not match any event.mouse*
- 🚫
: invalid;mouse.*.click
*
cannot be used in the middle of an event descriptor. - 🚫
: invalid;*.click
*
cannot be used in the prefix of an event descriptor. - 🚫
: invalid; does not match any event.mouse.click*
- 🚫
: invalid;mouse.*.*
*
cannot be used in the middle of an event descriptor.
Multiple transitions in parallel states
Since parallel states have multiple regions that can be active at the same time, it is possible for multiple transitions to be enabled at the same time. In this case, all enabled transitions to these regions will be taken.
Multiple targets are specified as an array of strings:
Coming soon… example.
Other transitions
- Eventless (always) transitions are transitions without events. These transitions are always taken after any transition in their state is enabled.
- Delayed (after) transitions are transitions that are enabled after a specified duration.
Transition descriptions
You can add a .description
string to a transition to describe the transition. This is useful for explaining the purpose of the transition in the visualized state machine.
import { createMachine } from 'xstate';
const feedbackMachine = createMachine({
// ...
on: {
exit: {
description: 'Closes the feedback form',
target: '.closed',
},
},
});
Shorthands
If the transition only specifies a target
, then the string target can be used as a shorthand instead of the entire transition object:
import { createMachine } from 'xstate';
const feedbackMachine = createMachine({
initial: 'prompt',
states: {
prompt: {
on: {
// This is shorthand for:
// 'feedback': { target: 'form' }
'feedback.good': 'thanks',
},
},
thanks: {},
// ...
},
});
Using the string target shorthand is useful for quickly prototyping state machines. Generally, we recommended using the full transition object syntax as it will be consistent with all other transition objects and will be easier to add actions, guards, and other properties to the transition in the future.
TypeScript
Transitions mainly use the event type that they are enabled by.
import { setup } from 'xstate';
const machine = setup({
types: {
events: {} as
| { type: 'greet'; message: string }
| { type: 'submit' }
}
}).createMachine({
// ...
on: {
greet: {
actions: ({ event }) => {
event.type; // 'greet'
event.message; // string
},
},
},
});
Frequently asked questions
How can I listen for events sent to actors?
You can use the inspection API to listen for all inspection events in an actor system. The @xstate.event
inspection event contains information about events sent from one actor to another (or itself):
import { createActor } from 'xstate';
import { someMachine } from './someMachine';
const actor = createActor(someMachine, {
inspect: (inspectionEvent) => {
if (inspectionEvent.type === '@xstate.event') {
// The event object sent from one actor to another
console.log(inspectionEvent.event);
}
}
});
Transitions cheatsheet
Use our XState events and transitions cheatsheet below to get started quickly.
Cheatsheet: event objects
feedbackActor.send({
// Event type
type: 'feedback.update',
// Event payload
feedback: 'A+ would use state machines again',
rating: 5,
});
Cheatsheet: transition targets
const machine = createMachine({
initial: 'a',
states: {
a: {
on: {
// Sibling target
event: {
target: 'b',
},
// Sibling child target
otherEvent: {
target: 'b.c',
},
},
},
b: {
on: {
// ID target
event: {
target: '#c',
},
},
},
c: {
id: 'c',
on: {
// Child target
event: {
target: '.child',
},
},
initial: 'child',
states: {
child: {},
},
},
},
on: {
// Child target
someEvent: {
target: '.b',
},
},
});