Stately
XState v6 alpha

Transitions

Configure event, delayed, eventless and completion transitions.

XState v6 is in alpha

APIs and behavior may change before the stable release.

A transition describes how a state responds to an event.

idle: { on: { start: { target: 'active' } } }

Transition properties

PropertyDescription
targetTarget state or states.
matchesEvent payload that must match.
contextContext patch or mapper.
inputInput for the target state.
reenterRe-enter the source state when targeting it.
metaPer-transition metadata.
descriptionHuman-readable description.

Use schemas.transitionMeta to type transition metadata separately from state metadata. If omitted, transition metadata uses schemas.meta for backwards compatibility.

schemas: {
  meta: z.object({ label: z.string() }),
  transitionMeta: z.object({ trackingId: z.number() })
}

There is no guard property. Conditions live inside the transition function, which returns undefined to reject the event. See guards.

A targetless transition can update context and run effects without leaving the current state. Set reenter: true when a self-transition should run exit and entry behavior again.

on: {
  rename: ({ context, event }) => ({
    context: { ...context, name: event.name }
  }),
  restart: { target: 'active', reenter: true }
}

Transition functions

submit: ({ context, event }, enq) => {
  if (!context.valid) return;
  enq(() => console.log('Submitted', event));
  return {
    target: 'submitting',
    context: { ...context, submittedAt: Date.now() }
  };
}

Returning undefined prevents the transition.

Transition functions must be synchronous. A transition function that returns a promise throws an execution error, which a state onError can recover. Move async work into an invoked or spawned actor, or into an effect enqueued with enq(...). The enq handle is only valid while the function runs: calling enq.* after the function returned throws in development and does nothing in production.

always runs without an external event. after runs after a delay. onDone, onError and onTimeout handle actor outcomes.

Use targetless transitions for edits that keep a form on the same step. Use re-entering transitions to restart a timer, subscription or invoked request.

Put shared transitions on a parent state. A child can set an event to undefined to forbid that parent transition. Wildcards such as pointer.* match an event family when no exact transition matches.

on: {
  'pointer.*': { target: 'tracking' },
  '*': { target: 'unexpectedEvent' }
}

Match event payloads

Internal lifecycle events use stable category types and carry the identity of what produced them:

Event typeIdentity
xstate.done.actoractorId, sessionId
xstate.error.actoractorId, sessionId
xstate.timeout.actoractorId, sessionId
xstate.done.statestateId
xstate.afterstateId, delay
xstate.timeoutstateId

Use matches to select one payload of an event type:

on: {
  'xstate.done.actor': {
    matches: { actorId: 'job' },
    target: 'complete'
  }
}

matches is a shallow partial pattern over the event's payload, compared by identity, so use it with primitive values. It is checked before the transition function runs. It works on any event, not only lifecycle events. onDone, onError, onTimeout and after set matches for you, which is how each one selects its own actor or state.

One transition per event

Transition arrays are not accepted by the authoring APIs. An event maps to a single transition. Return a target from a transition function to choose among several, and use matches to select a payload. Serialized transition arrays are still accepted by createMachineFromConfig(...).

on: {
  submit: ({ context }) =>
    context.role === 'admin'
      ? { target: 'adminReview' }
      : { target: 'standardReview' }
}

Unhandled events

An event is unhandled when no transition in the active states, including wildcard transitions, handles it. A transition function that returns undefined and enqueues nothing does not handle the event. An unhandled event leaves the actor unchanged.

The pure transition(logic, snapshot, event) API returns the same snapshot object and no effects for an unhandled event. A handled event always returns a new snapshot object, even when a transition function returns {}. Use isUnhandled(...) to check:

import { isUnhandled, transition } from 'xstate';

const result = transition(machine, snapshot, event);
if (isUnhandled(snapshot, result)) {
  // no transition handled `event`
}

A running actor reports an unhandled event through the onUnhandledEvent option of createActor(...). In the inspection stream, the @xstate.transition event for an unhandled event carries the unchanged snapshot reference. Development builds also log a warning once per event type per actor. Internal xstate.* events are not reported.

TypeScript

Transition targets are checked against authored state paths. Event schemas narrow event inside transition functions.

Transitions cheatsheet

on: { submit: { target: 'loading' } }
on: { rename: ({ context, event }) => ({ context: { ...context, name: event.name } }) }
on: { cancel: undefined }
always: { target: 'ready' }
after: { 1000: { target: 'idle' } }
on: { 'xstate.done.actor': { matches: { actorId: 'job' }, target: 'done' } }
onDone: { target: 'success' }
onError: { target: 'failure' }

On this page