Stately
XState v6 alpha

XState Effect: Matching states

Render workflow states with exhaustive Effect Match branches.

XState v6 is in alpha

APIs and behavior may change before the stable release.

taggedState(snapshot) gives a machine snapshot an Effect-style _tag. Use Match.tag and Match.exhaustive to describe every state of a workflow.

In this review example, only the approved state has a reviewer. Matching that tag also narrows its context:

import { Effect, Match, Schema, Stream } from 'effect';
import {
  createEffectActor,
  send,
  setupEffect,
  snapshots,
  taggedState,
  type TaggedState
} from '@xstate/effect';

const reviewMachine = setupEffect({
  states: {
    approved: {
      schemas: { context: Schema.Struct({ reviewer: Schema.String }) }
    }
  }
}).createMachine({
  initial: 'pending',
  states: {
    pending: {
      on: { APPROVE: { target: 'approved', context: { reviewer: 'Ada' } } }
    },
    approved: { type: 'final' }
  }
});

const describe = Match.type<TaggedState<typeof reviewMachine>>().pipe(
  Match.tag('pending', () => 'Waiting for review'),
  Match.tag('approved', ({ context }) => `Approved by ${context.reviewer}`),
  Match.exhaustive
);

const program = Effect.gen(function* () {
  const actor = yield* createEffectActor(reviewMachine);
  return [
    ...(yield* snapshots(actor).pipe(
      Stream.tap((s) =>
        s.matches('pending') ? send(actor, { type: 'APPROVE' }) : Effect.void
      ),
      Stream.map(taggedState),
      Stream.map(describe),
      Stream.runCollect
    ))
  ];
});

export const result = await Effect.runPromise(Effect.scoped(program));
console.log(result); // ['Waiting for review', 'Approved by Ada']

Adding a state requires adding a matching branch before Match.exhaustive will typecheck.

Tagged state fields

FieldDescription
_tagThe dotted state path, such as review.approved.
valueThe XState state value.
contextThe context for that state, including its per-state schema.
snapshotThe original snapshot.
  • TaggedState<typeof machine> names the union for a machine.
  • TaggedStateFrom<TSnapshot> names it for a snapshot type.
  • StateTag<TValue> names the tag for one state value.

Declare per-state context with setupEffect({ states }). See schemas and actions.

Parallel states

A parallel workflow can wait for review while building a release. Its _tag stops at the parallel state; when the root is parallel, the tag is '(machine)'. Inspect value or use snapshot.matches to read individual regions:

import { Effect, Match } from 'effect';
import {
  createEffectActor,
  taggedState,
  type TaggedState
} from '@xstate/effect';
import { createMachine } from 'xstate';

const releaseMachine = createMachine({
  type: 'parallel',
  states: {
    review: { initial: 'pending', states: { pending: {}, approved: {} } },
    build: { initial: 'running', states: { running: {}, passed: {} } }
  }
});

const describe = Match.type<TaggedState<typeof releaseMachine>>().pipe(
  Match.tag('(machine)', ({ snapshot }) =>
    snapshot.matches({ build: 'running' })
      ? 'Build in progress'
      : 'Build finished'
  ),
  Match.exhaustive
);

export const result = await Effect.runPromise(
  Effect.scoped(
    Effect.gen(function* () {
      const actor = yield* createEffectActor(releaseMachine);
      return describe(taggedState(actor.getSnapshot()));
    })
  )
);
console.log(result); // 'Build in progress'

UI atoms

createActorAtoms exposes this same tagged union through its state atom. A UI can match on states without calling taggedState itself. See atoms and React.

On this page