Advanced Patterns
Parallel states
A state can declare parallel regions — a set of independent sub-machines that all become active when the parent state is entered and are destroyed when it is left.
const editor = defineMachine({
id: 'editor',
initial: 'editing',
states: {
editing: {
parallel: {
saving: {
initial: 'idle',
states: {
idle: { on: { START_SAVE: { target: 'saving' } } },
saving: { on: { SAVE_DONE: { target: 'saved' } } },
saved: {},
},
},
validation: {
initial: 'valid',
states: {
valid: { on: { INVALIDATE: { target: 'invalid' } } },
invalid: { on: { VALIDATE: { target: 'valid' } } },
},
},
},
},
idle: {},
},
})send() delivers every event to all active regions. Each region handles it independently.
const { matches, send } = useMachine(editor)
matches('editing') // main state
matches({ saving: 'idle' }) // region check
matches({ validation: 'valid' }) // another region
await send('INVALIDATE')
matches({ validation: 'invalid' }) // true
matches({ saving: 'idle' }) // still true — unaffectedContext conflict resolution: when two regions return a Partial<context> that touches the same field, the last region in declaration order wins. A console.warn is emitted in dev mode naming the conflicting regions and field.
Limitation: parallel regions support one level of nesting. Regions cannot themselves contain parallel. This is a deliberate choice to control complexity.
useWizard
A composable for multi-step forms built on top of defineMachine. The wizard machine is generated automatically from the steps array.
function useWizard<TContext>(
steps: WizardStep<TContext>[],
options?: WizardOptions,
): WizardInstance<TContext>WizardStep
| Field | Type | Description |
|---|---|---|
id | string | Unique step identifier (becomes a state name internally) |
label | string? | Display label |
component | Component? | Vue component to render for this step |
canProceed | (ctx) => boolean | Promise<boolean> | Gate for next() and forward goTo(); may be async |
onEnter | (ctx) => void | Called when the wizard enters this step |
onLeave | (ctx) => void | Called when the wizard leaves this step |
WizardOptions
| Option | Type | Default | Description |
|---|---|---|---|
initialStep | number | 0 | Index of the starting step |
allowSkip | boolean | false | Skip canProceed on forward goTo() |
circular | boolean | false | next() wraps from last step back to first |
Return value
| Property | Type | Description |
|---|---|---|
currentStep | Ref<WizardStep> | Currently active step object |
currentIndex | ComputedRef<number> | Zero-based index of the current step |
totalSteps | number | Total number of steps |
progress | ComputedRef<number> | 0 to 1 based on current index |
isFirst | ComputedRef<boolean> | true on the first step |
isLast | ComputedRef<boolean> | true on the last step |
history | Ref<string[]> | IDs of visited steps |
next() | Promise<boolean> | Advance; calls canProceed first; returns false if blocked |
prev() | void | Go back (no guard) |
goTo(id) | Promise<boolean> | Jump to step by id; respects canProceed unless allowSkip |
reset() | void | Return to the initial step |
Example
<script setup lang="ts">
import { useWizard } from 'vue-state-machine'
import type { WizardStep } from 'vue-state-machine'
import StepInfo from './StepInfo.vue'
import StepAddress from './StepAddress.vue'
import StepPayment from './StepPayment.vue'
interface CheckoutCtx {
name: string
email: string
address: string
}
const steps: WizardStep<CheckoutCtx>[] = [
{
id: 'info',
label: 'Your info',
component: StepInfo,
canProceed: (ctx) => !!ctx.name && !!ctx.email,
},
{
id: 'address',
label: 'Delivery',
component: StepAddress,
canProceed: (ctx) => !!ctx.address,
},
{
id: 'payment',
label: 'Payment',
component: StepPayment,
onEnter: () => trackEvent('payment_step_entered'),
},
]
const { currentStep, progress, isFirst, isLast, next, prev } = useWizard(steps)
</script>
<template>
<div>
<progress :value="progress" max="1" />
<component :is="currentStep.component" />
<nav>
<button :disabled="isFirst" @click="prev">Back</button>
<button v-if="!isLast" @click="next">Next</button>
<button v-else @click="submit">Place order</button>
</nav>
</div>
</template>canProceed rules
- May return a
booleanor aPromise<boolean> - If it returns
false,next()/ forwardgoTo()returnfalseand the wizard stays on the current step - If it throws, the same outcome —
falseis returned, the error is logged toconsole.errorin dev mode prev()and backwardgoTo()never checkcanProceedallowSkip: truedisablescanProceedforgoTo()only;next()always checks it