Skip to content
Version: XState v4

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 the inactive state, it’ll trigger the onHover action. The wildcard won’t be called.
  • If the HOVER event is received in the active state, it’ll be caught by the wildcard above it and will logEventToConsole.
  • If the FOCUS event is received in any state, it’ll trigger the onFocus action, not the wildcard.

Wildcard transitions are great for logging untracked events or reducing code duplication.