Stately
XState v6 alpha

Global state

Run one application-wide actor and read it from any component.

XState v6 is in alpha

APIs and behavior may change before the stable release.

Some state belongs to the application rather than to a piece of the UI: the signed-in session, a notification queue, a media player that keeps playing while the user navigates. Create that actor once at module scope and read it with useSelector.

// player.ts
import { createActor } from 'xstate';

export const playerActor = createActor(playerMachine).start();
// TransportControls.tsx
import { useSelector } from '@xstate/react';
import { playerActor } from './player';

export function TransportControls() {
  const isPlaying = useSelector(playerActor, (s) => s.matches('playing'));

  return (
    <button onClick={() => playerActor.trigger.toggle()}>
      {isPlaying ? 'Pause' : 'Play'}
    </button>
  );
}

There is no provider and no hook that owns the actor. Any module can import the actor and send to it, including code outside React such as a router guard or a service worker message handler.

The actor is started at import time and never stopped, so it survives every unmount. That is the intended behavior for a media player. For state scoped to a screen, it is a leak.

Module scope or provider?

Module-scope actorcreateActorContext
LifetimeThe page sessionThe provider's mount
InstancesExactly oneOne per rendered provider
InputFixed at module loadPer provider, via options.input
Server renderingUnsafe — shared between requestsSafe — one actor per render
ResetManual, via an eventUnmount and remount

Choose a module-scope actor when there is exactly one of the thing and it should outlive any component. Choose a provider when the actor is scoped to a screen, a route, a dialog, or an item, or when it needs input that only a component knows.

Warning: Never create a module-scope actor in code that runs on the server. The module is shared by every request, so one user's state leaks into another's response. See server rendering.

Several global actors

Independent concerns get independent actors. A session actor, a notification actor and a player actor are three module-scope actors, each with its own file and its own selectors.

const unread = useSelector(notificationsActor, (s) => s.context.unread.length);
const userName = useSelector(sessionActor, (s) => s.context.user?.name);

When those actors must reach each other, put them in one actor system instead of importing one into the other. Declare the registry up front and look actors up by key from inside machine logic.

// system.ts
import { createSystem } from 'xstate';

export const appSystem = createSystem({
  registry: { notifications: notificationsMachine }
});

export const appActor = appSystem.createActor(appMachine).start();
// inside appMachine
on: {
  'order.placed': ({ system }, enq) => {
    enq.sendTo(system.get('notifications'), {
      type: 'notify',
      message: 'Order placed'
    });
  }
}

Components still read whichever actor holds the value they render:

const notifications = appActor.system.get('notifications');
const unread = useSelector(notifications, (s) => s?.context.unread.length ?? 0);

Registry entries are removed when their actor stops, so handle undefined.

A single root actor that invokes or spawns its children is usually clearer than several independent roots. Children then persist together in one getPersistedSnapshot() call, and one appActor.stop() tears everything down.

Global state cheatsheet

// module scope
export const appActor = createActor(appMachine, { input }).start();

// in a component
const value = useSelector(appActor, (snapshot) => snapshot.context.value);
appActor.trigger.refresh();

// systems
const system = createSystem({ registry: { notifications: notificationsMachine } });
const actor = system.createActor(appMachine).start();
system.get('notifications');

On this page