Stately
XState v6 alpha

Share actors

Provide one actor to Vue descendants with provide and inject.

XState v6 is in alpha

APIs and behavior may change before the stable release.

Create the actor once in a parent component, then share the reference with Vue's provide and inject. Descendants read what they need with useSelector(...).

A typed injection key

// checkoutKey.ts
import type { InjectionKey } from 'vue';
import type { ActorRefFrom } from 'xstate';
import { checkoutMachine } from './checkoutMachine';

export const checkoutKey: InjectionKey<ActorRefFrom<typeof checkoutMachine>> =
  Symbol('checkout');

An InjectionKey<T> carries the actor type, so inject(checkoutKey) is typed without casts.

Provide the actor

<!-- CheckoutProvider.vue -->
<script setup lang="ts">
import { provide } from 'vue';
import { useActorRef } from '@xstate/vue';
import { checkoutMachine } from './checkoutMachine';
import { checkoutKey } from './checkoutKey';

const props = defineProps<{ cartId: string }>();

const actorRef = useActorRef(checkoutMachine, { input: { cartId: props.cartId } });

provide(checkoutKey, actorRef);
</script>

<template><slot /></template>

useActorRef(...) is the right composable here: the provider itself does not render snapshot data, so it should not re-render on every transition.

Inject it

<script setup lang="ts">
import { inject } from 'vue';
import { useSelector } from '@xstate/vue';
import { checkoutKey } from './checkoutKey';

const checkoutRef = inject(checkoutKey)!;
const total = useSelector(checkoutRef, (snapshot) => snapshot.context.total);
const canPay = useSelector(checkoutRef, (snapshot) => snapshot.matches('review'));
</script>

<template>
  <output>{{ total }}</output>
  <button :disabled="!canPay" @click="checkoutRef.send({ type: 'pay' })">
    Pay
  </button>
</template>

Every consumer subscribes to the same actor and re-renders only when its own selection changes.

The actor's lifetime is the provider's lifetime: unmounting CheckoutProvider stops the actor and everything it invoked. Mount the provider at the level where the state should live: a cart above the checkout routes, or a session actor above the whole app shell.

Module-scope actors

An actor created at module scope lives for the lifetime of the page, independent of any component.

// sessionActor.ts
import { createActor } from 'xstate';
import { sessionMachine } from './sessionMachine';

export const sessionActor = createActor(sessionMachine).start();
const user = useSelector(sessionActor, (snapshot) => snapshot.context.user);

This suits one global concern per app, such as a session or a toast queue. It has real costs: the actor is never stopped, tests share state between cases, and on a server the module is shared by every request. Use provide/inject for anything scoped to a route, a request or a user. See Server rendering.

Actor systems

Give a shared actor a registryKey so any actor in the system can reach it without prop drilling.

const actorRef = useActorRef(checkoutMachine, { registryKey: 'checkout' });
// inside another machine's transition function
({ system }, enq) => {
  enq.sendTo(system.get('checkout'), { type: 'cart.cleared' });
};

On this page