Stately
XState v6 alpha

Emitted events

Emit events from an actor and handle them outside the machine.

XState v6 is in alpha

APIs and behavior may change before the stable release.

An actor emits events to tell the outside world that something happened. Use enq.emit(...) to emit, and actor.on(...) to handle.

const playerMachine = createMachine({
  schemas: {
    emitted: {
      played: z.object({ trackId: z.string() })
    }
  },
  initial: 'paused',
  states: {
    paused: {
      on: {
        play: ({ context }, enq) => {
          enq.emit({ type: 'played', trackId: context.trackId });
          return { target: 'playing' };
        }
      }
    },
    playing: {}
  }
});

const actor = createActor(playerMachine).start();

actor.on('played', (event) => {
  analytics.track('play', event.trackId);
});

actor.send({ type: 'play' });

Emitted events are delivered after the transition is applied. A handler that reads actor.getSnapshot() sees the state the transition moved to, not the state it came from.

Emitting

enq.emit(...) is available wherever enq is: transitions, entry actions and exit actions.

entry: (_, enq) => enq.emit({ type: 'opened' }),
exit: (_, enq) => enq.emit({ type: 'closed' })

Non-machine logic emits too. Custom logic and async logic use the same enq.emit(...):

const uploadLogic = createAsyncLogic({
  run: async ({ input }, enq) => {
    enq.emit({ type: 'upload.started', file: input.file });
    return await upload(input.file);
  }
});

Callback and observable logic emit through the emit function passed to them:

const socketLogic = createCallbackLogic(({ emit }) => {
  const socket = new WebSocket(url);
  socket.onmessage = (msg) => emit({ type: 'message', data: msg.data });
  return () => socket.close();
});

Handling

actor.on(type, handler) returns a subscription:

const subscription = actor.on('played', (event) => toast(event.trackId));

subscription.unsubscribe();

Use '*' to handle every emitted event, which is useful for logging and analytics:

actor.on('*', (event) => analytics.track(event.type, event));

Warning: actor.on(...) matches an exact event type or '*'. Prefix wildcards such as 'upload.*' do not match anything here. Use enq.listen(...) when a parent machine needs prefix matching.

An error thrown inside a handler is reported but does not stop the actor. Handlers are removed when the actor stops.

Emitted events, snapshots or sent events

  • Emit when the outside world should react to something that happened once: a toast, an analytics call, an imperative framework callback such as focusing an input or starting a video element.
  • Use a snapshot when the outside world renders current state. Snapshots describe current state. Emitted events describe something that happened once.
  • Use enq.sendTo(...) when one specific actor must receive the message. Emitting is broadcast: it has no target and no delivery guarantee if nobody is listening.

TypeScript

Declare emitted events in schemas.emitted, a map keyed by event type. enq.emit(...) then rejects unknown types and bad payloads, and actor.on(...) types the handler argument. A '*' handler receives the union of all emitted events.

schemas: {
  emitted: {
    'upload.progress': z.object({ percent: z.number() }),
    'upload.failed': z.object({ reason: z.string() })
  }
}

Without schemas.emitted, any event type is allowed and handler payloads are not checked.

Emitted events cheatsheet

// emit from a machine
on: {
  submit: (_, enq) => enq.emit({ type: 'order.placed', id: '1' });
}

// emit from other logic
createAsyncLogic({ run: async (_, enq) => enq.emit({ type: 'done' }) });
createCallbackLogic(({ emit }) => emit({ type: 'tick' }));

// handle
const sub = actor.on('order.placed', (event) => toast(event.id));
actor.on('*', (event) => console.log(event.type));
sub.unsubscribe();

On this page