| name | xstate-states-and-context |
| description | Covers XState v5 state types and context management. Use when implementing compound (parent/child) states, parallel states, history states, final states, or managing context with assign(). Includes hierarchy design, context initialization patterns, and state reading. |
XState v5 States and Context
Machine Creation
import { setup, createActor } from 'xstate';
const machine = setup({
types: {
context: {} as { count: number },
events: {} as { type: 'increment' } | { type: 'reset' },
},
actions: { },
guards: { },
actors: { },
}).createMachine({
id: 'counter',
initial: 'active',
context: { count: 0 },
states: { active: {} },
});
const actor = createActor(machine);
actor.subscribe((snapshot) => console.log(snapshot.value));
actor.start();
actor.send({ type: 'increment' });
State Types
Atomic States
Leaf states with no children — the simplest state type:
states: {
idle: {},
loading: {},
success: {},
}
Compound (Parent) States
States containing child states. Must have an initial property:
states: {
form: {
initial: 'editing',
states: {
editing: {
on: { VALIDATE: 'validating' },
},
validating: {
on: {
'validation.pass': 'valid',
'validation.fail': 'invalid',
},
},
valid: {},
invalid: {
on: { EDIT: 'editing' },
},
},
},
}
Parallel States
Independent regions that are all active simultaneously:
const machine = createMachine({
type: 'parallel',
states: {
upload: {
initial: 'idle',
states: {
idle: { on: { UPLOAD: 'uploading' } },
uploading: { on: { COMPLETE: 'done' } },
done: { type: 'final' },
},
},
form: {
initial: 'editing',
states: {
editing: { on: { SUBMIT: 'submitting' } },
submitting: {},
done: { type: 'final' },
},
},
},
onDone: 'allComplete',
});
Final States
Terminal states that signal completion to the parent:
states: {
processing: {
initial: 'step1',
states: {
step1: { on: { NEXT: 'step2' } },
step2: { on: { NEXT: 'complete' } },
complete: { type: 'final' },
},
onDone: { target: 'finished' },
},
finished: {},
}
Top-level final states cause the entire actor to complete, producing output:
const machine = createMachine({
states: {
done: {
type: 'final',
},
},
output: ({ context }) => ({ result: context.data }),
});
History States
Remember the last active child state:
states: {
settings: {
initial: 'general',
states: {
general: {},
privacy: {},
notifications: {},
hist: { type: 'history', history: 'shallow' },
},
on: { BACK: '.hist' },
},
}
history: 'shallow' — remembers the direct child state only
history: 'deep' — remembers the full nested state hierarchy
Parent/Child Patterns
When to Nest States
Nest when child states:
- Share transitions (defined on parent, handled regardless of child)
- Share invoked actors (invoked on parent, active in all children)
- Have a natural lifecycle (enter parent → process → exit parent)
Transition Selection (Deepest First)
Events are handled by the deepest matching state first, then bubble up:
const machine = createMachine({
initial: 'parent',
states: {
parent: {
initial: 'child',
on: {
EVENT: { actions: 'parentHandler' },
},
states: {
child: {
on: {
EVENT: { actions: 'childHandler' },
},
},
},
},
},
});
Parent onDone
The parent's onDone fires when a child reaches a final state:
states: {
wizard: {
initial: 'step1',
states: {
step1: { on: { NEXT: 'step2' } },
step2: { on: { NEXT: 'done' } },
done: { type: 'final' },
},
onDone: 'complete',
},
complete: {},
}
Modular State Configs (v5.21+)
Break large machines into separate files:
const machineSetup = setup({ });
const editingState = machineSetup.createStateConfig({
on: {
VALIDATE: 'validating',
SAVE: { actions: 'saveDraft' },
},
});
const validatingState = machineSetup.createStateConfig({
invoke: {
src: 'validateForm',
onDone: 'valid',
onError: 'invalid',
},
});
const machine = machineSetup.createMachine({
initial: 'editing',
states: { editing: editingState, validating: validatingState, valid: {}, invalid: {} },
});
Context Management
Static Initial Context
createMachine({
context: {
count: 0,
items: [],
user: null,
},
});
Lazy Initial Context
Evaluated per actor instance — good for timestamps, random IDs:
createMachine({
context: () => ({
id: crypto.randomUUID(),
createdAt: Date.now(),
items: [],
}),
});
Input-Based Context
Use input to parameterize machines (replaces factory functions):
const machine = setup({
types: {
context: {} as { userId: string; rating: number },
input: {} as { userId: string; defaultRating: number },
},
}).createMachine({
context: ({ input }) => ({
userId: input.userId,
rating: input.defaultRating,
}),
});
const actor = createActor(machine, {
input: { userId: '123', defaultRating: 5 },
});
Updating Context with assign()
Property assigners (preferred — partial update):
on: {
INCREMENT: {
actions: assign({
count: ({ context }) => context.count + 1,
}),
},
'item.add': {
actions: assign({
items: ({ context, event }) => [...context.items, event.item],
}),
},
}
Function assigners (for dynamic/full updates):
on: {
RESET: {
actions: assign(({ context }) => ({
...context,
count: 0,
error: null,
})),
},
}
Critical: Never mutate context directly. Always return new values:
actions: assign({
items: ({ context, event }) => {
context.items.push(event.item);
return context.items;
},
})
actions: assign({
items: ({ context, event }) => [...context.items, event.item],
})
Reading State
const actor = createActor(machine).start();
const snapshot = actor.getSnapshot();
snapshot.value;
snapshot.context;
snapshot.matches('loading');
snapshot.matches({ form: 'editing' });
snapshot.hasTag('loading');
snapshot.can({ type: 'SUBMIT' });
snapshot.status;
snapshot.output;
snapshot.children;
Prefer hasTag() over matches() — tags survive refactoring:
states: {
loading: { tags: ['busy'] },
submitting: { tags: ['busy'] },
refreshing: { tags: ['busy'] },
}
Anti-Patterns
Booleans Instead of States
context: { isLoading: false, isError: false, data: null }
states: { idle: {}, loading: {}, success: {}, error: {} }
Unnecessary Nesting
states: {
wrapper: {
initial: 'only',
states: { only: {} },
},
}
states: { only: {} }
Context Mutation
assign({ items: ({ context }) => { context.items.push(x); return context.items; } })
assign({ items: ({ context }) => [...context.items, x] })