Skip to content

Instantly share code, notes, and snippets.

@chrisdmacrae
Last active February 10, 2020 03:36
Show Gist options
  • Save chrisdmacrae/33ef61af35d6f595730be983358dcb49 to your computer and use it in GitHub Desktop.
Save chrisdmacrae/33ef61af35d6f595730be983358dcb49 to your computer and use it in GitHub Desktop.
Builder patterns for XState

Overview

XState at it's core is a configuration object and an interpretter for that object (to create the actual instance of that machine configuration).

This should not change. Instead, a abstraction on top of this to make the configuration easier to discover and learn is what I propose.

Goal

Make it easier to discover and learn XState through a highly typed, conversational API for generating state machine configurations.

Usage

I see there being two approaches for this using the builder pattern:

  1. The builder pattern, using a builder class/object to compose a state machine configuration, to be passed to the interpretter:
  2. Through strict dot-chaining, with a proper fluent interface (get, set, add, remove)
  3. Through composition, using multiple fluent assertions

Nerd stuff

None of this should be required -- it should be a builder class/object implemented separately that simply outputs the valid object configuration.

Resolutions should be lazy -- not evaluated until interpretations are run. This allows architecture that allows one setter to call a getter for something that isn't technically set if you are reading the code procedurally. (e.g, calling machine.getState()) on a later defined state.

Dot chaining is totally optional. The logic can be written as statement per line.

// Compare fluent to object notation
const pedestrianMachine = Machine({
id: "pedestrian",
initial: 'walk',
states: {
walk: {
on: {
PED_TIMER: 'wait'
}
},
wait: {
on: {
PED_TIMER: 'stop'
}
},
stop: {}
}
});
////// VERSUS
export const pedestrianMachineBuilder = MachineBuilder("pedestrian", machine =>
machine
.addState("walk", state => state.addTransition("PED_TIMER", machine.getState("wait"))
.addState("wait", state => state.addTransition("PED_TIMER", machine.getState("stop"))
.addState("stop")
.setInitialState(machine.getState("walk");
);
const pedestrianMachine = pedestrianMachineBuilder.createMachine();
////// OR
export const pedestrianMachineBuilder = MachineBuilder("pedestrian", machine => {
machine.addState("walk", state => state.addTransition("PED_TIMER", machine.getState("wait"))
machine.addState("wait", state => state.addTransition("PED_TIMER", machine.getState("stop"))
machine.addState("stop")
machine.setInitialState(machine.getState("walk");
return machine;
});
const pedestrianMachine = pedestrianMachineBuilder.createMachine();
import { MachineBuilder } from "xstate";
export const pedestrianMachineBuilder = MachineBuilder("pedestrian", machine => {
machine
.addState("walk", state => state.addTransition("PED_TIMER", machine.getState("wait"))
.addState("wait", state => state.addTransition("PED_TIMER", machine.getState("stop"))
.addState("stop")
.setInitialState(machine.getState("walk");
return machine;
});
// composition with many fluent assertions
// (pretty, makes breaking changes easier to deal with)
export const trafficLightMachineBuilder = MachineBuilder("traffic_light", machine => {
let states = machine
.addState("green", state => state.addTransition("TIMER", machine.getState("yellow"))
.addState("yellow", state => state.addTransition("TIMER", machine.getState("red"))
.addState("red", state => state.addTransition("TIMER", machine.getState("red"))
.getStates();
machine.setInitialState(greenState);
machine.setState(states.map(state => state.addTransition("RESET", machine.getState("green")));
machine.getState("red").addState(pedestrianMachineBuilder);
return machine;
});
export const trafficLightMachine = interpret(trafficLightMachineBuilder);
import { MachineBuilder, MachineSettings, StateSettings, TransitionSettings } from "xstate";
// chained approach using proper fluent interface
// (ugly, but easy to learn, makes breaking changes easier to deal with)
const trafficLightMachineBuilder = MachineBuilder("traffic_light", (machine: MachineSettings) =>
machine
.addState("green", (state: StateSettings) =>
state
.addTransition("TIMER", (transition: TransitionSettings) => transition.onEvent("TIMER", machine.getState("yellow"));
.addState("yellow", (state: StateSettings) =>
state
.addTransition("TIMER", (transition: TransitionSettings) => transition.onEvent("TIMER", machine.getState("red"));
.addState("red", (state: StateSettings) =>
state
.addTransition("TIMER", (transition: TransitionSettings) => transition.onEvent("TIMER", machine.getState("green"));
});
// Simplified, w/o type annotations
const trafficLightMachineBuilder = MachineBuilder("traffic_light", machine =>
machine
.addState("green", state =>
state
.addTransition("TIMER", (transition: TransitionSettings) => transition.onEvent("TIMER", machine.getState("yellow"));
.addState("yellow", (state: StateSettings) =>
state
.addTransition("TIMER", (transition: TransitionSettings) => transition.onEvent("TIMER", machine.getState("red"));
.addState("red", (state: StateSettings) =>
state
.addTransition("TIMER", (transition: TransitionSettings) => transition.onEvent("TIMER", machine.getState("green"));
});
export const trafficLightMachine = interpret(trafficLightMachineBuilder);
import { pedestrianMachineBuilder, trafficLightMachineBuilder } from "./composition.ts";
// Get object output from builder
const pedestrianMachineConfig = pedestrianMachineBuilder.getConfig();
console.log(pedestrianMachineConfig);
// {
// initial: 'walk',
// states: {
// walk: {
// on: {
// PED_TIMER: 'wait'
// }
// },
// wait: {
// on: {
// PED_TIMER: 'stop'
// }
// },
// stop: {}
// }
// };
// Create an instance directly from builder
export const trafficLightMachine = trafficLightMachineBuilder.createMachine();
// Create an instance with interpreter
export const trafficLightMachine = interpret(trafficLightMachineBuilder);
// Create an instance directly w/ Machine function
const trafficLightMachineConfig = trafficLightMachineBuilder.getConfig();
export const trafficLightMachine = Machine(trafficLightMachineConfig);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment