Skip to content

Продвинутые паттерны

Параллельные состояния

Состояние может объявлять регионы parallel — набор независимых под-машин, которые все становятся активными при входе в родительское состояние и уничтожаются при выходе из него.

ts
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() доставляет каждое событие во все активные регионы. Каждый регион обрабатывает его независимо.

ts
const { matches, send } = useMachine(editor)

matches('editing') // основное состояние
matches({ saving: 'idle' }) // проверка региона
matches({ validation: 'valid' }) // другой регион

await send('INVALIDATE')
matches({ validation: 'invalid' }) // true
matches({ saving: 'idle' }) // всё ещё true — не затронуто

Разрешение конфликтов контекста: когда два региона возвращают Partial<context>, затрагивающий одно и то же поле, побеждает последний регион в порядке объявления. В dev-режиме выводится console.warn с именами конфликтующих регионов и полем.

Ограничение: параллельные регионы поддерживают один уровень вложенности. Регионы не могут сами содержать parallel. Это осознанное решение для контроля сложности.

useWizard

Composable для многошаговых форм, построенный поверх defineMachine. Машина wizard'а генерируется автоматически из массива шагов.

ts
function useWizard<TContext>(
  steps: WizardStep<TContext>[],
  options?: WizardOptions,
): WizardInstance<TContext>

WizardStep

ПолеТипОписание
idstringУникальный идентификатор шага (внутри становится именем состояния)
labelstring?Отображаемая метка
componentComponent?Vue-компонент, рендерящийся для этого шага
canProceed(ctx) => boolean | Promise<boolean>Условие для next() и goTo() вперёд; может быть асинхронным
onEnter(ctx) => voidВызывается, когда wizard входит в этот шаг
onLeave(ctx) => voidВызывается, когда wizard покидает этот шаг

WizardOptions

ОпцияТипПо умолчаниюОписание
initialStepnumber0Индекс начального шага
allowSkipbooleanfalseПропускать canProceed при goTo() вперёд
circularbooleanfalsenext() возвращается от последнего шага к первому

Возвращаемое значение

СвойствоТипОписание
currentStepRef<WizardStep>Объект текущего активного шага
currentIndexComputedRef<number>Индекс текущего шага (от нуля)
totalStepsnumberОбщее число шагов
progressComputedRef<number>От 0 до 1 на основе текущего индекса
isFirstComputedRef<boolean>true на первом шаге
isLastComputedRef<boolean>true на последнем шаге
historyRef<string[]>ID посещённых шагов
next()Promise<boolean>Продвигает вперёд; сначала вызывает canProceed; возвращает false, если заблокировано
prev()voidНазад (без guard)
goTo(id)Promise<boolean>Переход к шагу по id; учитывает canProceed, если не allowSkip
reset()voidВозврат к начальному шагу

Пример

vue
<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

  • Может возвращать boolean или Promise<boolean>
  • Если возвращает false, next() / goTo() вперёд возвращают false, и wizard остаётся на текущем шаге
  • Если выбрасывает исключение, результат тот же — возвращается false, ошибка логируется в console.error в dev-режиме
  • prev() и goTo() назад никогда не проверяют canProceed
  • allowSkip: true отключает canProceed только для goTo(); next() проверяет его всегда