Communicating Between Machines

Expressing the entire app's behavior in a single machine can quickly become complex and unwieldy. It is natural (and encouraged!) to use multiple machines that communicate with each other to express complex logic instead. This closely resembles the Actor model, where each machine instance is considered an "actor" that can send and receive events (messages) to and from other machine "actors" and react to them.

For machines to communicate, the parent machine invokes a child machine and listens to events sent from the child machine via sendParent(...), or waits for the child machine to reach its final state, which will then cause the onDone transition to be taken:

import { Machine, actions } from 'xstate';
import { interpret } from 'xstate/lib/interpreter';
const { send, sendParent } = actions;

const minuteMachine = Machine({
  id: 'timer',
  initial: 'active',
  states: {
    active: {
      after: {
        60000: 'finished'
      }
    },
    finished: { type: 'final' }
  }
});

const parentMachine = Machine({
  id: 'parent',
  initial: 'pending',
  states: {
    pending: {
      invoke: {
        src: minuteMachine,
        onDone: 'timesUp'
      }
    },
    timesUp: {
      type: 'final'
    }
  }
});

const service = interpret(parentMachine)
  .onTransition(state => console.log(state.value))
  .start();
// => 'pending'
// ... after 1 minute
// => 'timesUp'

The invoke Property

An invocation is defined in a state node's configuration with the invoke property, whose value is an object that contains:

  • src - the source of the machine to invoke, which can be:

    • a machine
    • a string, which refers to a machine defined in this machine's options.services
    • a function that returns a Promise
  • id - the unique identifier for the invoked service
  • forward - (optional) true if all events sent to this machine should also be sent (or forwarded) to the invoked child machine (false by default)
  • params - (optional) an object that maps properties of the child machine's context to a function that returns the corresponding value from the parent machine's context.
  • onDone - (optional) the transition to be taken when the child machine reaches its final state
  • onError - (optional) the transition to be taken when the child machine encounters an execution error.

Sending Events

Statecharts communicate hierarchically:

  • Parent-to-child via the send(EVENT, { target: 'someChildId' }) action
  • Child-to-parent via the sendParent(EVENT) action.

Here is an example of two statecharts, pingMachine and pongMachine, communicating with each other via a supervisor parentMachine, which invokes both child machines:

import { Machine, actions } from 'xstate';
import { interpret } from 'xstate/lib/interpreter';
const { send, sendParent } = actions;

const pingMachine = Machine({
  id: 'ping',
  initial: 'active',
  states: {
    active: {
      onEntry: sendParent('PING'),
      on: {
        PONG: {
          actions: sendParent('PING', {
            delay: 1000
          })
        }
      }
    }
  }
});

const pongMachine = Machine({
  id: 'pong',
  initial: 'active',
  states: {
    active: {
      on: {
        PING: {
          actions: sendParent('PONG', {
            delay: 1000
          })
        }
      }
    }
  }
});

const parentMachine = Machine({
  id: 'parent',
  initial: 'active',
  states: {
    active: {
      invoke: [pingMachine, pongMachine],
      on: {
        PING: {
          actions: send('PING', { to: pongMachine.id })
        },
        PONG: {
          actions: send('PONG', { to: pingMachine.id })
        }
      }
    }
  }
});

const service = interpret(parentMachine).start();


// => 'ping'
// ...
// => 'pong'
// ..
// => 'ping'
// ...
// => 'pong'
// ...

Let's make the classic traffic light example more real-life and model the behavior of two traffic lights at an intersection:

const northLightMachine = Machine({
  id: 'north',
  initial: 'green',
  states: {
    green: {
      after: { 1000: 'yellow' }
    },
    yellow: {
      after: { 1000: 'red' }
    },
    red: {
      // Notify parent that 'red' state was reached
      onEntry: sendParent('NORTH_RED'),
      on: { SAFE_SIGNAL: 'green' }
    }
  }
});

const eastLightMachine = Machine({
  id: 'east',
  initial: 'red',
  states: {
    green: {
      after: { 1000: 'yellow' }
    },
    yellow: {
      after: { 1000: 'red' }
    },
    red: {
      // Notify parent that 'red' state was reached
      onEntry: sendParent('EAST_RED'),
      // Wait until it's safe to turn green
      on: { SAFE_SIGNAL: 'green' }
    }
  }
});

const superLightMachine = Machine({
  id: 'supervisor',
  initial: 'active',
  invoke: [
    { src: northLightMachine },
    { src: eastLightMachine },
  ],
  states: {
    active: {
      on: {
        EAST_RED: {
          // tell 'north' machine it is safe to turn green
          actions: send('SAFE_SIGNAL', { to: 'north' })
        },
        NORTH_RED: {
          // tell 'east' machine it is safe to turn green
          actions: send('SAFE_SIGNAL', { to: 'east' })
        }
      }
    }
  }
});

// parent supervisor machine
const interpretedSuperLight = interpret(superLightMachine);