Use a machine in React
Run XState actor logic in a React component.
XState v6 is in alpha
Install XState and the React package.
npm install xstate@alpha @xstate/react@alphauseActor(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:
useActortakes actor logic, not an actor reference. Passing an existingactorRefthrows in development. Read from an existing actor withuseSelector.
Actor lifecycle
One actor is created per component instance. It starts in an effect after mount and is stopped on unmount.
React StrictMode mounts, unmounts and remounts a component in development. The second mount would otherwise leave the component holding a stopped actor, so useActor and useActorRef detect an externally stopped actor and create a fresh one from the same logic and options, starting from the initial state. An actor that completed naturally (done or error) is left alone and is not restarted.
That development-only stop and restart does not currently restart invoked or spawned children. If a child actor appears inert only under StrictMode, that is why; the behavior in production builds is unaffected.
Changing the logic identity between renders is handled separately: when the config of the logic passed in differs from the running actor's, a new actor is created from the current logic and seeded with the previous actor's persisted snapshot, so the component keeps its state. Implementations swapped with machine.provide({ ... }) in the component body keep the same config, so they update the running actor in place rather than replacing it. A guard or action defined in render always sees the latest props.
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));