Skip to content
Version: XState v5

Parent 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. Parent states are also known as compound states.

In the video player above, the Opened state is a parent state to the PlayingPaused, and Stopped states. These states, their transitions, and their events are nested inside the Opened state.

Root state

The state machine itself is a parent state! It’s the root state, and it’s always active.

It’s normal to have a state machine that has no other states. This is useful for modeling a simple state machine that only handles events by executing actions in transitions.

Here is an example of a simple counting machine with increment, decrement, and reset events, and no states, other than the implicit top-level root state:

import { createMachine } from 'xstate';

const countingMachine = createMachine({
id: 'counting',
on: {
increment: {
actions: assign({ count: ({ context }) => context.count + 1 }),
},
decrement: {
actions: assign({ count: ({ context }) => context.count - 1 }),
},
reset: {
actions: assign({ count: 0 }),
},
},
// No child states!
});

Initial state

The initial state of a parent state is the state that is entered when the parent state is entered. Parent states must have an initial states.

You specify the initial state via the initial property of the parent state, which is the key of the initial state in the states object:

import { createMachine } from 'xstate';

const feedbackMachine = createMachine({
initial: 'question',
states: {
question: {
// ...
},
form: {
// ...
},
thanks: {
// ...
},
},
});

Even if the parent state is never directly targeted and its child states are instead targeted, specifying the initial state in the .initial property is required. In this case, the .initial property can be any of the child states.

Transitions on parent states

A transition that targets a parent state will enter the parent state and its initial state. If that initial state is a parent state, then that state’s initial state will be entered, and so on.

When an event is received, transitions on the deepest child nodes are checked first to see if any of them are enabled by that event. If no transitions are enabled, then transitions on the parent state are checked. If no transitions on the parent state are enabled, then transitions on the parent's parent state is checked, and so on.

Transitions on a parent state can target child (or descendent) states. This is useful for modeling a transition that should go to a specific child state regardless of which child state is currently active.

Transitions on a child state can target the parent state, though this is not common. A transition from a child state to its parent (or ancestor) state will also enter the parent state’s initial state.

Child final states

When a child final state of a parent state is reached, that parent state is considered "done". The onDone transition of that parent state is automatically taken.

import { createMachine } from 'xstate';

const coffeeMachine = createMachine({
initial: 'preparation',
states: {
preparation: {
initial: 'weighing',
states: {
weighing: {
on: {
weighed: {
target: 'grinding'
}
}
},
grinding: {
on: {
ground: 'ready'
}
},
ready: {
// Child final state of parent state 'preparation'
type: 'final'
}
},
// Transition will be taken when child final state is reached
onDone: {
target: 'brewing'
}
},
brewing: {
// ...
}
}
});

Modeling

Coming soon

  • Start with a flat structure; don’t create parent states too early
  • If many states have common outgoing transitions, that’s a good sign for putting them in a parent state
  • Parent states also represent sub-processes.

Parent states cheatsheet

Cheatsheet: creating parent states

// The machine is the root-level parent state
const machine = createMachine({
// Initial child state of the machine
initial: 'parent',
states: {
parent: {
// Initial child state of the parent state
initial: 'child1',
states: {
child1: {
on: {
// Targeting a sibling
toSibling: {
target: 'child2',
},
},
},
child2: {
initial: 'grandchild1',
states: {
grandchild1: {},
grandchild2: {},
},
},
},
on: {
// Targeting a child
toChild: {
target: '.child1',
},
// Targeting a grandchild
toGrandchild: {
target: '.child2.grandchild2',
},
},
},
},
});