AI Workshop: learn to build apps with AI →
Other JavaScript Libraries: XState

Join the AI Workshop and learn to build real-world apps with AI. A hands-on, practical program to level up your skills.


XState

XState is a popular JavaScript library for working with finite state machines.

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 project using various tools to help us design it 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.

Installation

Install XState using npm:

npm install xstate

Then import it in your program:

import { Machine, interpret } from 'xstate'

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

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

This will make a global XState variable on the window object.

Creating a state machine

Use the Machine factory function to create a state machine:

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

Here we defined 3 states: green, yellow, and red.

Defining transitions

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

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

When we’re in the green state and we get a TIMER event, we switch to yellow, and so on.

Triggering transitions

You can get the initial state:

machine.initialState.value //'green'

And switch to a new state using the transition() method:

const currentState = machine.initialState.value
const newState = machine.transition(currentState, 'TIMER')
console.log(newState.value)

Using services

With transition() you always have to keep track of the current state. To avoid this, create a service using interpret():

const toggleService = interpret(machine).start()

Now you can use send() to trigger a transition:

const toggleService = interpret(machine).start()
toggleService.send('TIMER')

After sending an event, the service’s state is updated. You can read it from the service:

const newState = toggleService.send('TIMER')
console.log(newState.value)

Multiple transitions from one state

From a state, you can see what events will trigger a state change using its nextEvents property.

Here’s a more complex example with multiple transitions:

const machine = Machine({
  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'
      }
    },
  }
})

Use it:

const toggleService = interpret(machine).start();
toggleService.send('p1').value //'l1'
toggleService.send('p1').value //'l2'
toggleService.send('p1').value //'nolights'

Adding actions

To do something when we switch to a new state, use actions:

const machine = Machine({
  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')
    }
  }
})

Each state transition can specify a target (the new state) and actions to run.

You can run multiple actions by passing an array of strings.

You can also define actions inline:

{
  on: {
    p1: {
      target: 'l1',
      actions: (context, event) => {
        console.log('turnOnL1')
      }
    }
  }
}

This is just scratching the surface of XState. Check the XState Docs for more advanced usage.

Lessons in this unit:

0: Introduction
1: jQuery
2: Axios
3: Moment.js
4: SWR
5: ▶︎ XState
6: PeerJS