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 Playing, Paused, and Stopped states. These states, their transitions, and their events are nested inside the Opened state.
Add a child state
First, select your machine or an existing state where you want to create the child state. Then…
- Use the + State button that appears above the selected state to create a nested child state. Or…
- If a state already contains child states, you can double-click inside the parent state to create another child state.
Change the parent state of a child state
Using the State details panel:
- Select the child state you want to reparent.
- Open the State details panel from the right tool menu.
- Choose your desired new parent from the Parent dropdown menu.
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 via transitions:
Example of a simple counting machine with increment, decrement, reset, and no states:
import { createMachine } from 'xstate';
const countingMachine = createMachine({
id: 'counting',
initial: 'active',
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.
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. Very useful when
TypeScript
Coming soon
Cheatsheet
// 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',
},
},
},
},
});