Introduction to XState

By

An overview of XState, the JavaScript library for finite state machines, covering how to install it and define a machine with createMachine and createActor.

~~~

I wrote about finite state machines in the past and I mentioned XState. In this post I want to introduce this popular JavaScript library.

Finite state machines are an interesting way to tackle complex state and state changes and keep your code bugs-free as much as possible.

Just as we model a software projects using various tools to help us design it before building it, and we use mockups and UX tools to think about an UI before building it, finite state machines help us solve state transitions.

Computer programs are all about transitioning from one state to another after an input. Things can get out of control if you’re not paying close attention, and XState is a very helpful tool to help us navigate the state complexity as it grows.

This post uses XState 5, the stable release. Skip the v6 alpha. The v4 names Machine and interpret are gone: use createMachine and createActor instead.

You install XState using npm:

npm install xstate

then you can import it in your program using the ES Modules syntax. As a minimum you typically import createMachine and createActor:

import { createMachine, createActor } from 'xstate'

In the browser you can also import it from a CDN directly:

<script src="https://unpkg.com/xstate@5/dist/xstate.umd.min.js"></script>

and this will make a global XState variable on the window object (with XState.createMachine, XState.createActor, and so on).

Next you can define a finite state machine using the createMachine factory function. This function accepts a configuration object, and returns a reference to the newly created state machine:

const machine = createMachine({

})

In the configuration we pass an id string that identifies the state machine, the initial state string. Here is a simple traffic lights example:

const machine = createMachine({
  id: 'trafficlights',
  initial: 'green'
})

We also pass a states object containing the allowed states:

const machine = createMachine({
  id: 'trafficlights',
  initial: 'green',
  states: {
    green: {

    },
    yellow: {

    },
    red: {

    }
  }
})

Here I defined 3 states: green yellow and red.

To transition from one state to another we will send a message to the machine, and it will know what to do based on the configuration we set.

Here we set to switch to the yellow state when we’re in the green state and we get a TIMER event:

const machine = createMachine({
  id: 'trafficlights',
  initial: 'green',
  states: {
    green: {
      on: {
        TIMER: 'yellow'
      }
    },
    yellow: {

    },
    red: {

    }
  }
})

I called it TIMER because traffic lights usually have a simple timer that changes the lights state every X seconds.

Now let’s fill the other 2 state transitions: we go from yellow to red, and from red to green:

const machine = createMachine({
  id: 'trafficlights',
  initial: 'green',
  states: {
    green: {
      on: {
        TIMER: 'yellow'
      }
    },
    yellow: {
      on: {
        TIMER: 'red'
      }
    },
    red: {
      on: {
        TIMER: 'green'
      }
    }
  }
})

How do we trigger a transition?

Create an actor from the machine with createActor, start it, then send events. In XState 5, events are objects with a type property (string event names alone are no longer accepted):

const actor = createActor(machine).start()

actor.getSnapshot().value // 'green' in our case

actor.send({ type: 'TIMER' })
console.log(actor.getSnapshot().value) // 'yellow'

send() does not return the next state anymore. Read it with getSnapshot() after you send, or subscribe:

const actor = createActor(machine)
actor.subscribe(snapshot => {
  console.log(snapshot.value)
})
actor.start()
actor.send({ type: 'TIMER' })

This is just scratching the surface of XState.

From a state you can go to multiple states depending on the trigger you get.

In the case of traffic lights, this is not something that will happen, but let’s model the house lights example we had in the finite state machines post:

Diagram showing a room with three lights l1, l2, l3 and two push buttons p1, p2 for controlling the lights

When you enter the house, you can press one of the 2 push buttons you have, p1 or p2. When you press any of those buttons, the l1 light turns on.

Imagine this is the entrance light, and you can take your jacket off. Once you are done, you decide which room you want to go into (kitchen or bedroom, for example).

If you press the button p1, l1 turns off and l2 turns on. Instead if you press the button p2, l1 turns off and l3 turns on.

Pressing another time any of the 2 buttons, p1 or p2, the light that is currently on will turn off, and we’ll get back at the initial state of the system.

Here is our XState machine object:

const machine = createMachine({
  id: 'roomlights',
  initial: 'nolights',
  states: {
    nolights: {
      on: {
        p1: 'l1',
        p2: 'l1'
      }
    },
    l1: {
      on: {
        p1: 'l2',
        p2: 'l3'
      }
    },
    l2: {
      on: {
        p1: 'nolights',
        p2: 'nolights'
      }
    },
    l3: {
      on: {
        p1: 'nolights',
        p2: 'nolights'
      }
    },
  }
})

Now we can create an actor and send it messages:

const actor = createActor(machine).start()
actor.send({ type: 'p1' })
actor.getSnapshot().value // 'l1'
actor.send({ type: 'p1' })
actor.getSnapshot().value // 'l2'
actor.send({ type: 'p1' })
actor.getSnapshot().value // 'nolights'

One thing we miss here is how do we do something when we switch to a new state. That is done through actions, which we define in a second object parameter we pass to the createMachine() factory function.

In XState 5, action implementations receive a single argument object ({ context, event }), not separate context and event parameters:

const machine = createMachine({
  id: 'roomlights',
  initial: 'nolights',
  states: {
    nolights: {
      on: {
        p1: {
          target: 'l1',
          actions: 'turnOnL1'
        },
        p2: {
          target: 'l1',
          actions: 'turnOnL1'
        }
      }
    },
    l1: {
      on: {
        p1: {
          target: 'l2',
          actions: 'turnOnL2'
        },
        p2: {
          target: 'l3',
          actions: 'turnOnL3'
        }
      }
    },
    l2: {
      on: {
        p1: {
          target: 'nolights',
          actions: ['turnOffAll']
        },
        p2: {
          target: 'nolights',
          actions: ['turnOffAll']
        }
      }
    },
    l3: {
      on: {
        p1: {
          target: 'nolights',
          actions: 'turnOffAll'
        },
        p2: {
          target: 'nolights',
          actions: 'turnOffAll'
        }
      }
    },
  }
}, {
  actions: {
    turnOnL1: ({ context, event }) => {
      console.log('turnOnL1')
    },
    turnOnL2: ({ context, event }) => {
      console.log('turnOnL2')
    },
    turnOnL3: ({ context, event }) => {
      console.log('turnOnL3')
    },
    turnOffAll: ({ context, event }) => {
      console.log('turnOffAll')
    }
  }
})

See how now each state transition defined in the object passed to on instead of being just a string it’s an object with the target property (where we pass the string we used before) and we also have an actions property where we can set the action to run.

We can run multiple actions by passing an array of strings instead of a string.

And you can also define the action(s) directly on the actions property instead of “centralizing” them into a separate object:

const machine = createMachine({
  id: 'roomlights',
  initial: 'nolights',
  states: {
    nolights: {
      on: {
        p1: {
          target: 'l1',
          actions: ({ context, event }) => {
            console.log('turnOnL1')
          },
          ...

But in this case it’s handy to put them all together because similar actions are fired by different state transitions.

That’s it for this tutorial. I recommend you to check out the XState Docs for more advanced usage of XState, but it’s a start.

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about js: