Stately
XState v6 alpha

Use a machine in Solid

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

XState v6 is in alpha

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

@xstate/solid provides four primitives: useActor(...), useActorRef(...), fromActorRef(...) and useMachine(...). They require Solid 1.6 or later.

import { Show } from 'solid-js';
import { useActor } from '@xstate/solid';
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 }
        })
      }
    }
  }
});

export function MediaPlayer() {
  const [snapshot, send] = useActor(playerMachine);

  return (
    <>
      <button onClick={() => send({ type: 'play' })}>Play</button>
      <Show when={snapshot.matches('playing')}>
        <output>Volume: {snapshot.context.volume}</output>
      </Show>
    </>
  );
}

What useActor returns

useActor(...) returns a tuple, not an object.

IndexValueDescription
0snapshotA deeply reactive snapshot — a Solid store, not an accessor.
1sendSends an event to the actor.
2actorRefThe running actor, for subscribe(...), getPersistedSnapshot() or passing to children.

The snapshot is read directly, as in snapshot.context.volume or snapshot.matches('playing'), with no call parentheses. Each property access is its own reactive dependency, so a component that reads snapshot.context.volume does not re-run when an unrelated part of context changes.

useActor(...) accepts any actor logic, not only machines: a machine, an async logic, or a callback logic.

Warning: Destructuring the snapshot (const { context } = snapshot) reads the value once and loses reactivity, as with any Solid store. Keep the property access inside JSX, a createMemo(...) or an effect.

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: props.fileId },
  registryKey: 'upload'
});

Options are read once, when the component runs. Changing the values later does not restart the actor. Send an event instead, or recreate the component with a <Show keyed> or <Key> boundary.

Lifecycle

The actor is created when the component function runs, started in onMount, and stopped in onCleanup.

  • 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 cleanup 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.

useActorRef

useActorRef(logic, options?) returns only the actor, with the same lifecycle and no reactive snapshot.

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

Unlike the Vue package, there is no observer or listener argument. Call actorRef.subscribe(...) yourself and unsubscribe in onCleanup(...), or use fromActorRef(...).

useMachine

useMachine(machine, options?) is an alias of useActor(...) restricted to machine logic, returning the same tuple. New code can use useActor(...) for everything.

TypeScript

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

import type { ActorRefFrom } from 'xstate';

function Total(props: { actorRef: ActorRefFrom<typeof checkoutMachine> }) {
  const snapshot = fromActorRef(() => props.actorRef);
  return <output>{snapshot().context.total}</output>;
}

Solid primitives cheatsheet

const [snapshot, send, actorRef] = useActor(logic, options);
const actorRef = useActorRef(logic, options);
const snapshot = fromActorRef(actorRef); // Accessor
const [snapshot, send, actorRef] = useMachine(machine, options); // alias

On this page