Stately
XState v6 alpha

Use a machine in React

Run XState actor logic in a React component.

XState v6 is in alpha

APIs and behavior may change before the stable release.

Install XState and the React package.

npm install xstate@alpha @xstate/react@alpha

useActor(logic, options?) creates an actor for the component, starts it, and re-renders when it produces a new snapshot.

import { useActor } from '@xstate/react';
import { createMachine } from 'xstate';

const playerMachine = createMachine({
  initial: 'paused',
  states: {
    paused: { on: { play: { target: 'playing' } } },
    playing: { on: { pause: { target: 'paused' } } }
  }
});

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

  return (
    <button onClick={() => send({ type: 'play' })}>
      {snapshot.matches('playing') ? 'Playing' : 'Play'}
    </button>
  );
}

The returned tuple is [snapshot, send, actorRef]. The third member is the full actor, useful for actorRef.trigger, actorRef.on(...) and passing a reference to children.

const [snapshot, send, actorRef] = useActor(playerMachine);

<button onClick={() => actorRef.trigger.play()}>Play</button>;

actorRef.trigger is typed from the machine's schemas.events. See TypeScript.

Options

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

const [snapshot, send] = useActor(uploadMachine, {
  input: { fileId },
  inspect: (event) => console.log(event)
});

Options are read when the actor is created. Changing input on a later render does not restart the actor — see input and snapshots.

Re-render semantics

useActor subscribes with useSyncExternalStore, so the component re-renders whenever the actor produces a new snapshot object. An event that takes no transition produces the same snapshot reference, and React bails out of the render.

That still means every state or context change re-renders the component. When a component only needs one value from a long-lived actor, use useActorRef(...) with useSelector(...) instead.

const actorRef = useActorRef(playerMachine);
const isPlaying = useSelector(actorRef, (s) => s.matches('playing'));

useActorRef(logic, options?, observerOrListener?) creates and starts an actor without subscribing to it, so it never re-renders on its own. The optional third argument is a snapshot listener function or an observer object, subscribed for as long as it is provided. Memoize it with useCallback; a new function identity re-subscribes.

const onSnapshot = useCallback(
  (snapshot) => {
    if (snapshot.status === 'done') navigate('/receipt');
  },
  [navigate]
);

const actorRef = useActorRef(checkoutMachine, undefined, onSnapshot);

If the actor reaches an error snapshot, useActor throws the error during render, so an error boundary can catch it. useActorRef does not.

Warning: useActor takes actor logic, not an actor reference. Passing an existing actorRef throws in development. Read from an existing actor with useSelector.

Actor lifecycle

One actor is created per component instance. It starts in an effect after mount and is stopped on unmount.

React StrictMode disconnects and immediately reconnects effects in development. useActor and useActorRef defer cleanup by one microtask, so that immediate reconnect cancels the pending stop and preserves the actor and its children. A real unmount still stops the actor. If an actor was stopped externally before an effect reconnects, the hooks create a fresh actor from the same logic and options; an actor that completed naturally (done or error) is left alone.

The logic passed on the first render is used for the component's lifetime, like a useState initializer. Passing a different machine on a later render does not replace the actor, so a machine created in the component body needs no useMemo. Implementations swapped with machine.provide({ ... }) in the component body keep the same config and update the running actor in place, so a guard or action defined in render always sees the latest props. To vary a running actor, pass input or send an event. To switch to a different machine, remount the component with a key:

<Editor key={mode} machine={mode === 'draft' ? draftMachine : reviewMachine} />

Hot reloading

In development builds, when React Fast Refresh re-renders a component after the machine's module was edited, the hooks keep the running actor and switch it to the edited machine. The current state and context carry over in memory, without serializing, so context that holds DOM elements or cyclic objects is kept. If an active state gained child states, their initial states become active. Invoked and spawned actors whose logic did not change keep running; the others restart.

The hooks start a fresh actor from the edited machine instead when:

  • the current state no longer exists in it, or its id changed
  • a remembered history state no longer exists in it
  • an invoked actor's src is a function, or names an actor it does not provide
  • context or a pending delayed event refers to an actor that would restart
  • its configured validator rejects the current context

Production builds never switch machines.

useMachine(...) is a deprecated alias for useActor(machine, options). It accepts state machines only. Use useActor(...).

TypeScript

Snapshot, event and actor reference types are inferred from the actor logic. options becomes a required argument when the logic requires input.

import type { SnapshotFrom } from 'xstate';

const [snapshot, send] = useActor(playerMachine);
snapshot.context; // typed
send({ type: 'play' }); // typed

type PlayerSnapshot = SnapshotFrom<typeof playerMachine>;

React hooks cheatsheet

const [snapshot, send, actorRef] = useActor(logic, options);
const actorRef = useActorRef(logic, options, observerOrListener);
const value = useSelector(actorRef, (snapshot) => snapshot.context.value);

send({ type: 'play' });
actorRef.trigger.play();
actorRef.on('played', (event) => analytics.track(event));

On this page