Parent and child states
States can contain more states, also known as child states. These child states are only active when the parent state is active. Child states are nested inside their parent states.
In XState, you can specify a parent state using the states
attribute on child nodes:
const machine = createMachine({
initial: 'waiting',
states: {
waiting: {
on: {
'leave home': {
target: 'on a walk',
},
},
},
'on a walk': {
initial: 'walking',
on: {
'arrive home': {
target: 'walk complete',
},
},
states: {
walking: {
on: {
'speed up': {
target: 'running',
},
stop: {
target: 'stopping to sniff good smells',
},
},
},
running: {
on: {
'slow down': {
target: 'walking',
},
},
},
'stopping to sniff good smells': {
on: {
'speed up': {
target: 'walking',
},
},
},
},
},
'walk complete': {},
},
});
Note in the example above that transitions can be marked at different levels of the state hierarchy. In this example, the machine can receive the arrive home
event in any of the child states inside on a walk
.
However, the machine can only receive the stop
event inside on a walk.walking
. Events can be handled differently at different levels of the statechart, which is powerful for handling complex requirements.
The root node
After learning about parent states, you might have noticed that all statecharts are parent states! Every statechart has a single root state which you can treat just like any other state.
For example, you can listen to events on the root state:
const machine = createMachine({
on: {
GREETED: {
actions: 'sayHello',
},
},
initial: 'idle',
states: {
idle: {},
working: {},
},
});
Any time the machine receives the GREETED
event, no matter which state it’s in, it’ll run the sayHello
action.
Entry and exit actions are also useful in the root state:
const machine = createMachine({
entry: ['sayHello'],
exit: ['sayGoodbye'],
initial: 'idle',
states: {
idle: {},
working: {},
},
});
In the example above, the machine will sayHello
when it starts running and sayGoodbye
when it stops running.
You can also omit all states altogether! Sometimes you just need a root state, some events and some actions:
import { createMachine } from 'xstate';
const machine = createMachine({
entry: ['sayHello'],
exit: ['sayGoodbye'],
});
Everything that works inside a state — after
, always
, invoke
(we’ll cover these later), entry
, exit
and more — will work inside the root node.
on
in parent states
When a child state cannot handle an event
, that event
is propagated up to its parent state (including the root node) to be handled.
In the example below, when you wave at your friend and the machine is in the friendIsLookingAtYou
state, the friendWavesBack
action is fired.
import { createMachine } from 'xstate';
const waveMachine = createMachine({
on: {
WAVE_AT_YOUR_FRIEND: {
actions: 'feelEmbarrassed',
},
},
initial: 'friendIsLookingAtYou',
states: {
friendIsLookingAtYou: {
on: {
WAVE_AT_YOUR_FRIEND: {
actions: 'friendWavesBack',
},
},
},
friendIsNotLookingAtYou: {},
friendIsNotWhoYouThoughtTheyWere: {},
},
});
If the machine is in the friendIsNotLookingAtYou
or friendIsNotWhoYouThoughtTheyWere
states, the event is propagated up to its parent state, the root node, and the feelEmbarrassed
action is fired. We’ve all been there.
Adding transitions in the parent state helps reduce duplication when defining transitions.
Wildcard transitions
A transition specified with a wildcard *
is triggered by any event not already handled by the current state.
The following example shows a few different cases of when the wildcard *
is triggered or not.
import { createMachine } from 'xstate';
const machine = createMachine({
initial: 'inactive',
on: {
'*': {
actions: 'logEventToConsole',
},
FOCUS: {
actions: 'onFocus',
},
},
states: {
inactive: {
on: {
HOVER: {
actions: 'onHover',
},
},
},
active: {},
},
});
- If the
HOVER
event is received in theinactive
state, it’ll trigger theonHover
action. The wildcard won’t be called. - If the
HOVER
event is received in theactive
state, it’ll be caught by the wildcard above it and willlogEventToConsole
. - If the
FOCUS
event is received in any state, it’ll trigger theonFocus
action, not the wildcard.
Wildcard transitions are great for logging untracked events or reducing code duplication.