Stately
XState v6 alpha

Share actors

Share one actor through Solid context.

XState v6 is in alpha

APIs and behavior may change before the stable release.

Create the actor once in a provider component, then share the reference with Solid's createContext. Descendants read what they need with fromActorRef(...).

A typed context

// checkoutContext.tsx
import { createContext, useContext, type ParentProps } from 'solid-js';
import { useActorRef } from '@xstate/solid';
import type { ActorRefFrom } from 'xstate';
import { checkoutMachine } from './checkoutMachine';

const CheckoutContext = createContext<ActorRefFrom<typeof checkoutMachine>>();

export function CheckoutProvider(props: ParentProps<{ cartId: string }>) {
  const actorRef = useActorRef(checkoutMachine, {
    input: { cartId: props.cartId }
  });

  return (
    <CheckoutContext.Provider value={actorRef}>
      {props.children}
    </CheckoutContext.Provider>
  );
}

export function useCheckout() {
  const actorRef = useContext(CheckoutContext);
  if (!actorRef) throw new Error('useCheckout must be used inside CheckoutProvider');
  return actorRef;
}

createContext<T>() without a default value gives useContext(...) the type T | undefined. Throwing in the hook narrows it once, so no consumer needs a non-null assertion.

useActorRef(...) is the right primitive here: the provider renders no snapshot data, so it should not depend on the actor's transitions.

Read it in a descendant

import { createMemo, Show } from 'solid-js';
import { fromActorRef } from '@xstate/solid';
import { useCheckout } from './checkoutContext';

export function PayButton() {
  const checkoutRef = useCheckout();
  const snapshot = fromActorRef(checkoutRef);
  const canPay = createMemo(() => snapshot().matches('review'));

  return (
    <>
      <output>{snapshot().context.total}</output>
      <Show when={canPay()}>
        <button onClick={() => checkoutRef.send({ type: 'pay' })}>Pay</button>
      </Show>
    </>
  );
}

Each consumer subscribes to the same actor, and Solid's fine-grained updates mean only the expressions that read a changed value re-run.

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 session = fromActorRef(sessionActor);

This suits one global browser-only concern, such as a session or a toast queue. The costs are real: the actor is never stopped, tests share state between cases, and in SolidStart the module is shared by every server request. Use context 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 passing references down the tree.

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