# Interpreting Machines

While a state machine/statechart with a pure .transition() function is useful for flexibility, purity, and testability, in order for it to have any use in a real-life application, something needs to:

  • Keep track of the current state, and persist it
  • Execute side-effects
  • Handle delayed transitions and events
  • Communicate with external services

The interpreter is responsible for interpreting the state machine/statechart and doing all of the above - that is, parsing and executing it in a runtime environment. An interpreted, running instance of a statechart is called a service.

# Interpreter 4.0+

An optional interpreter is provided that you can use to run your statecharts. The interpreter handles:

  • State transitions
  • Executing actions (side-effects)
  • Delayed events with cancellation
  • Activities (ongoing actions)
  • Invoking/spawning child statechart services
  • Support for multiple listeners for state transitions, context changes, events, etc.
  • And more!
import { createMachine, interpret } from 'xstate';

const machine = createMachine(/* machine config */);

// Interpret the machine, and add a listener for whenever a transition occurs.
const service = interpret(machine).onTransition((state) => {
  console.log(state.value);
});

// Start the service
service.start();

// Send events
service.send({ type: 'SOME_EVENT' });

// Stop the service when you are no longer using it.
service.stop();

# Sending Events

Events are sent to a running service by calling service.send(event). There are 3 ways an event can be sent:





 


 



 

service.start();

// As an object (preferred):
service.send({ type: 'CLICK', x: 40, y: 21 });

// As a string:
// (same as service.send({ type: 'CLICK' }))
service.send('CLICK');

// As a string with an object payload:
// (same as service.send({ type: 'CLICK', x: 40, y: 21 }))
service.send('CLICK', { x: 40, y: 21 });
  • As an event object (e.g., .send({ type: 'CLICK', x: 40, y: 21 }))
    • The event object must have a type: ... string property.
  • As a string (e.g., .send('CLICK'), which resolves to sending { type: 'CLICK' })
    • The string represents the event type.
  • As a string followed by an object payload (e.g., .send('CLICK', { x: 40, y: 21 })) 4.5+
    • The first string argument represents the event type.
    • The second argument must be an object without a type: ... property.

WARNING

If the service is not initialized (that is, if service.start() wasn't called yet), events will be deferred until the service is started. This means that the events won't be processed until service.start() is called, and then they will all be sequentially processed.

This behavior can be changed by setting { deferEvents: false } in the service options. When deferEvents is false, sending an event to an uninitialized service will throw an error.

# Batched Events

Multiple events can be sent as a group, or "batch", to a running service by calling service.send(events) with an array of events:

service.send([
  // String events
  'CLICK',
  'CLICK',
  'ANOTHER_EVENT',
  // Event objects
  { type: 'CLICK', x: 40, y: 21 },
  { type: 'KEYDOWN', key: 'Escape' }
]);

This will immediately schedule all batched events to be processed sequentially. Since each event causes a state transition that might have actions to execute, actions in intermediate states are deferred until all events are processed, and then they are executed with the state they were created in (not the end state).

This means that the end state (after all events are processed) will have an .actions array of all of the accumulated actions from the intermediate states. Each of these actions will be bound to their respective intermediate states.

WARNING

Only one state -- the end state (i.e., the resulting state after all events are processed) -- will be sent to the .onTransition(...) listener(s). This makes batched events an optimized approach for performance.

TIP

Batched events are useful for event sourcing (opens new window) approaches. A log of events can be stored and later replayed by sending the batched events to a service to arrive at the same state.

# Transitions

Listeners for state transitions are registered via the .onTransition(...) method, which takes a state listener. State listeners are called every time a state transition (including the initial state) happens, with the current state instance:

// Interpret the machine
const service = interpret(machine);

// Add a state listener, which is called whenever a state transition occurs.
service.onTransition((state) => {
  console.log(state.value);
});

service.start();

TIP

If you only want the .onTransition(...) handler(s) to be called when the state changes (that is, when the state.value changes, the state.context changes, or there are new state.actions), use state.changed (opens new window):


 




service.onTransition((state) => {
  if (state.changed) {
    console.log(state.value);
  }
});

TIP

The .onTransition() callback will not run between eventless ("always") transitions or other microsteps. It only runs on macrosteps. Microsteps are the intermediate transitions between macrosteps.

# Starting and Stopping

The service can be initialized (i.e., started) and stopped with .start() and .stop(). Calling .start() will immediately transition the service to its initial state. Calling .stop() will remove all listeners from the service, and do any listener cleanup, if applicable.

const service = interpret(machine);

// Start the machine
service.start();

// Stop the machine
service.stop();

// Restart the machine
service.start();

Services can be started from a specific state by passing the state into service.start(state). This is useful when rehydrating the service from a previously saved state.

// Starts the service from the specified state,
// instead of from the machine's initial state.
service.start(previousState);

# Executing Actions

Actions (side-effects) are, by default, executed immediately when the state transitions. This is configurable by setting the { execute: false } option (see example). Each action object specified on the state might have an .exec property, which is called with the state's context and event object.

Actions can be executed manually by calling service.execute(state). This is useful when you want to control when actions are executed:

const service = interpret(machine, {
  execute: false // do not execute actions on state transitions
});

service.onTransition((state) => {
  // execute actions on next animation frame
  // instead of immediately
  requestAnimationFrame(() => service.execute(state));
});

service.start();

# Options

The following options can be passed into the interpreter as the 2nd argument (interpret(machine, options)):

  • execute (boolean) - Signifies whether state actions should be executed upon transition. Defaults to true.
  • deferEvents (boolean) 4.4+ - Signifies whether events sent to an uninitialized service (i.e., prior to calling service.start()) should be deferred until the service is initialized. Defaults to true.
    • If false, events sent to an uninitialized service will throw an error.
  • devTools (boolean) - Signifies whether events should be sent to the Redux DevTools extension (opens new window). Defaults to false.
  • logger - Specifies the logger to be used for log(...) actions. Defaults to the native console.log method.
  • clock - Specifies the clock interface for delayed actions. Defaults to the native setTimeout and clearTimeout functions.

# Custom Interpreters

You may use any interpreter (or create your own) to run your state machine/statechart. Here's an example minimal implementation that demonstrates how flexible interpretation can be (despite the amount of boilerplate):

const machine = createMachine(/* machine config */);

// Keep track of the current state, and start
// with the initial state
let currentState = machine.initialState;

// Keep track of the listeners
const listeners = new Set();

// Have a way of sending/dispatching events
function send(event) {
  // Remember: machine.transition() is a pure function
  currentState = machine.transition(currentState, event);

  // Get the side-effect actions to execute
  const { actions } = currentState;

  actions.forEach((action) => {
    // If the action is executable, execute it
    typeof action.exec === 'function' && action.exec();
  });

  // Notify the listeners
  listeners.forEach((listener) => listener(currentState));
}

function listen(listener) {
  listeners.add(listener);
}

function unlisten(listener) {
  listeners.delete(listener);
}

// Now you can listen and send events to update state
listen((state) => {
  console.log(state.value);
});

send('SOME_EVENT');

# Notes

  • The interpret function is exported directly from xstate since 4.3+ (i.e., import { interpret } from 'xstate'). For prior versions, it is imported from 'xstate/lib/interpreter'.
  • Most interpreter methods can be chained:
const service = interpret(machine)
  .onTransition((state) => console.log(state))
  .onDone(() => console.log('done'))
  .start(); // returns started service
  • Do not call service.send(...) directly from actions. This impedes testing, visualization, and analysis. Instead, use invoke.
Last Updated: 12/13/2021, 11:00:34 AM