Skip to content

Defining Machines ​

defineMachine(config) is a pure factory function. Validates the config and returns it with improved TypeScript types. Zero Vue dependency — can be called and tested in Node without a Vue app.

ts
function defineMachine<TState, TEvent, TContext>(
  config: MachineConfig<TState, TEvent, TContext>,
): MachineConfig<TState, TEvent, TContext>

Config shape ​

ts
const machine = defineMachine({
  id: 'login', // unique identifier (required, used by DevTools and MachineStore)
  initial: 'idle', // starting state
  context: {
    // optional initial context (deep-cloned per instance)
    attempts: 0,
    error: null as string | null,
  },
  states: {
    idle: {
      on: {
        // event name → transition config
        SUBMIT: { target: 'loading', actions: [resetError] },
      },
    },
    loading: {
      on: {
        SUCCESS: { target: 'success' },
        FAILURE: { target: 'error', actions: [incrementAttempts] },
      },
    },
    error: {
      on: {
        RETRY: { target: 'idle', guard: canRetry },
      },
    },
    success: { type: 'final' }, // terminal — further send() calls are no-ops
  },
})

State config options ​

on ​

Record<TEvent, TransitionConfig>. Event handlers.

entry ​

Action[]. Invoked when the machine enters this state.

exit ​

Action[]. Invoked when the machine leaves this state.

type ​

'final'. Terminal state — isDone becomes true, send() is ignored.

parallel ​

Record<string, SubMachineConfig>. Parallel regions — see Parallel States.

Transition config options ​

target ​

TState. Destination state (TypeScript-checked against config).

guard ​

Guard<TContext, TEvent>. Synchronous predicate; false or a thrown exception blocks the transition.

actions ​

Action<TContext, TEvent>[]. Side-effects executed during the transition.

Dev-time validation ​

In development (import.meta.env.DEV !== false), defineMachine throws descriptive errors for:

  • Empty config.id
  • initial not found in states
  • Any target referencing a non-existent state

Validation is tree-shaken away in production builds.

Guards and Actions ​

ts
type Guard<TContext, TEvent> = (context: TContext, event: EventObject<TEvent>) => boolean

type Action<TContext, TEvent> = (
  context: TContext,
  event: EventObject<TEvent>,
) => void | Partial<TContext> | Promise<Partial<TContext> | void>

Guard rules:

  • Must be synchronous and side-effect-free — it is also called by can() reactively
  • An exception thrown inside a guard is caught and treated as false
  • Promise return values are not awaited — use actions for async work

Action rules:

  • May be async — the event queue awaits each action before executing the next
  • Return Partial<TContext> to merge updates into context; return void for side-effects only
  • Actions execute in order: exit → transition.actions → entry
ts
// Action that updates context
const incrementAttempts = (ctx: { attempts: number }) => ({
  attempts: ctx.attempts + 1,
})

// Async action — fetch result is merged into context
const loadUser = async (ctx, event: { type: 'LOAD'; id: number }) => {
  const user = await api.getUser(event.id)
  return { user }
}

// Guard
const canRetry = (ctx: { attempts: number }) => ctx.attempts < 3