Stately
XState v6 alpha

Use a machine in Vue

Run XState logic in a Vue component with useActor and useActorRef.

XState v6 is in alpha

APIs and behavior may change before the stable release.
npm install xstate@alpha @xstate/vue@alpha

@xstate/vue provides four composables: useActor(...), useActorRef(...), useSelector(...) and useMachine(...). They require Vue 3.

<script setup lang="ts">
import { useActor } from '@xstate/vue';
import { createMachine } from 'xstate';

const playerMachine = createMachine({
  context: { volume: 5 },
  initial: 'paused',
  states: {
    paused: { on: { play: { target: 'playing' } } },
    playing: {
      on: {
        pause: { target: 'paused' },
        volume: ({ context, event }) => ({
          context: { ...context, volume: event.level }
        })
      }
    }
  }
});

const { snapshot, send } = useActor(playerMachine);
</script>

<template>
  <button @click="send({ type: 'play' })">Play</button>
  <output>{{ snapshot.value }}</output>
</template>

What useActor returns

MemberTypeDescription
snapshotRef<SnapshotFrom<TLogic>>Shallow ref holding the latest snapshot.
send(event) => voidSends an event to the actor.
actorRefActor<TLogic>The running actor, for subscribe(...), getPersistedSnapshot() or passing to children.

snapshot is a ref, so read snapshot.value in script code. Vue unwraps top-level refs in templates, which is why {{ snapshot.value }} in a template renders the snapshot's state value, not the ref.

useActor(...) accepts any actor logic: a machine, a promise logic, or a callback logic. It throws in development if it receives an actor reference instead of logic; use useSelector(actorRef, ...) for that.

Options

The second argument is the same actor options object accepted by createActor(...): input, snapshot, id, registryKey, clock, logger, inspect. It is required when the logic requires input.

const { snapshot, send } = useActor(uploadMachine, {
  input: { fileId },
  registryKey: 'upload'
});

Options are read once, when the component sets up. Changing the values later does not restart the actor. Send an event instead.

Lifecycle

The actor is created during setup(), started in onMounted, and stopped in onBeforeUnmount.

  • The initial snapshot is readable before mount, so the first render is never empty.
  • Events sent before mount are queued in the actor's mailbox and processed once it starts.
  • On unmount the actor is stopped: invoked children stop, delayed transitions are cancelled and further events are ignored.

State does not survive unmount. Persist the snapshot if the actor should resume later. See Input and restored snapshots.

Warning: These composables register lifecycle hooks, so call them synchronously in setup() or <script setup>. Calling them in an event handler leaves the actor unstarted and unstopped.

useActorRef

useActorRef(logic, options?, observerOrListener?) returns only the actor, with the same lifecycle. No snapshot ref is created, so the component does not re-render on every transition.

const actorRef = useActorRef(checkoutMachine, { input: { cartId } }, (s) => {
  console.log(s.value);
});

The third argument is an observer or a listener function. It is subscribed on mount and unsubscribed on unmount.

Pair it with useSelector(...) when a component needs one value from a large actor, and with provide/inject when descendants need the same actor.

useMachine

useMachine(machine, options?) is an alias of useActor(...) restricted to machine logic. It returns the same { snapshot, send, actorRef } object. New code can use useActor(...) for everything.

TypeScript

Snapshot, event and input types are inferred from the logic. Type a component prop that carries an actor with ActorRefFrom:

import type { ActorRefFrom } from 'xstate';

const props = defineProps<{
  actorRef: ActorRefFrom<typeof checkoutMachine>;
}>();

If the machine requires input, TypeScript requires the options argument with an input property.

Vue composables cheatsheet

const { snapshot, send, actorRef } = useActor(logic, options);
const actorRef = useActorRef(logic, options, observerOrListener);
const value = useSelector(actorRef, (snapshot) => snapshot.context.value);
const { snapshot, send, actorRef } = useMachine(machine, options); // alias

On this page