Skip to content

Instantly share code, notes, and snippets.

@customcommander
Created January 5, 2023 10:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save customcommander/06176eba335d6b126b3b144a35374f85 to your computer and use it in GitHub Desktop.
Save customcommander/06176eba335d6b126b3b144a35374f85 to your computer and use it in GitHub Desktop.
Observing state transitions with XState & RxJS

Observing state transitions with XState & RxJS

Objective

Create an observable that emits each time a specific transition is taken.

Problem

This "flip" machine keeps tossing the coin each time it lands on head. The machine reaches its final state when the side is tail:

stateDiagram-v2
  state after_flip <<choice>>
  [*] --> flip
  flip --> after_flip
  after_flip --> head
  head --> flip
  after_flip --> tail
  tail --> [*]

We want to display how many times the coin landed on head so far each time the machine makes a transition to head e.g.,

flipping…
head. flip again.
current head count: 1
head. flip again.
current head count: 2
head. flip again.
current head count: 3
head. flip again.
current head count: 4
tail. stop.

If the coin lands on tail on the first throw:

flipping…
tail. stop.

Solution

The flip machine can be implemented as follow:

const flip = interpret(createMachine({
  initial: 'flip',
  states: {
    flip: {
      always: [
        {target: 'head', cond: () => Math.random() < 0.8},
        {target: 'tail'}
      ]
    },
    head: {
      entry: log(() => 'head. flip again.'),
      after: {
        50: {
          target: 'flip'
        }
      }
    },
    tail: {
      type: 'final',
      entry: log(() => 'tail. stop.')
    }
  }
}));

We'll start by creating an observable that emits when the machine reaches its final state.

const tail$ =
  fromEventPattern(
    // how to get values
    handler => flip.onDone(handler),
    // how to stop getting values
    handler => flip.off(handler))
  .pipe(take(1) /* <- unsubscribe after first emission */);

Note: We don't actually care about what that observable emits. We just use it as a signal to unsubscribe from the other observable.

Now let's create an observable that emits each time the machine takes a transition to head:

const head$ =
  fromEventPattern(
    // how to get values
    handler => {
      flip.onTransition(st => {
        if (st.matches('head')) {
          handler(1); // <- value emitted by the observable. see `n` below
        }
      });
    },
    // how to stop getting values
    handler => flip.off(handler))
  .pipe(
    scan((tot, n) => tot + n, 0),
    takeUntil(tail$) /* <- unsubscribe as soon as `tail$` emits */);

We can now run the machine and subscribe to head$:

console.log('flipping…');
flip.start();
head$.subscribe(tot => {
  console.log(`current head count: ${tot}`);
});

Full Script

import { createMachine, interpret } from "xstate";
import { fromEventPattern, scan, take, takeUntil } from "rxjs";
import { log } from "xstate/lib/actions.js";

const flip = interpret(createMachine({
  initial: 'flip',
  states: {
    flip: {
      always: [
        {target: 'head', cond: () => Math.random() < 0.8},
        {target: 'tail'}
      ]
    },
    head: {
      entry: log(() => 'head. flip again.'),
      after: {
        50: {
          target: 'flip'
        }
      }
    },
    tail: {
      type: 'final',
      entry: log(() => 'tail. stop.')
    }
  }
}));

const tail$ =
  fromEventPattern(
    // how to get values
    handler => flip.onDone(handler),
    // how to stop getting values
    handler => flip.off(handler))
  .pipe(take(1) /* <- unsubscribe after first emission */);

const head$ =
  fromEventPattern(
    // how to get values
    handler => {
      flip.onTransition(st => {
        if (st.matches('head')) {
          handler(1); // <- value emitted by the observable. see `n` below
        }
      });
    },
    // how to stop getting values
    handler => flip.off(handler))
  .pipe(
    scan((tot, n) => tot + n, 0),
    takeUntil(tail$) /* unsubscribe as soon as `tail$` emits */);

console.log('flipping…');
flip.start();
head$.subscribe(tot => {
  console.log(`current head count: ${tot}`);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment