Hierarchical States
Problem
Related states share common transitions and data but differ in their sub-behaviour. Flattening them into sibling states leads to duplicated event handlers and state explosion.
Solution
Use compound states to group related substates. Entering a compound state automatically resolves to its initial child. Shared transitions on the parent apply to all substates:
ts
import { machine } from '@vielzeug/clockwork';
type Event = { type: 'CANCEL' } | { type: 'EDIT' } | { type: 'SAVE' } | { type: 'SAVED' };
type Context = { draft: string };
const editor = machine({
context: { draft: '' },
initial: 'idle',
states: {
idle: {
on: { EDIT: { target: 'editing' } },
},
editing: {
initial: 'draft',
states: {
draft: {
on: {
CANCEL: { target: 'idle' },
SAVE: { target: 'editing.saving' },
},
},
saving: {
invoke: [
{
src: async ({ context, signal }) => {
await fetch('/api/save', { body: context.draft, method: 'POST', signal });
},
onDone: () => ({ type: 'SAVED' }),
onError: () => ({ type: 'CANCEL' }),
},
],
on: {
SAVED: { target: 'idle' },
CANCEL: { target: 'editing.draft' },
},
},
},
},
},
});
const m = editor;
console.log(m.state.value); // 'idle'
m.send({ type: 'EDIT' });
console.log(m.state.value); // 'editing.draft' (auto-resolved to initial leaf)
m.matches('editing'); // true — ancestor match
m.matches('editing.draft'); // true — exact match
m[Symbol.dispose]();Pitfalls
matches('parent')returnstruefor any child state — usestate.valuefor exact matching,matches()for ancestor checks.- Transitions on a compound state apply to all children — if you add a transition on the parent, every substate will handle that event. Use child-level
onto restrict scope.