Model web component behaviour with XState

ducksoupdev

Matt Levy

Posted on July 20, 2022

Model web component behaviour with XState

XState is a library for creating, interpreting, and executing finite state machines and statecharts.

Finite state machines (or 'state machines' for short) and statecharts are a visual language used to describe the states in a process. You may have used similar diagrams in the past to design user flows, plan databases or map application architecture. They are perfect for modelling web component behaviour.

State machines and statecharts provide a visual way of using boxes and arrows to represent behaviour. These flows are also executable code that can be used to control the logic in your application and that makes it more predicable and testable.

In this post, we're going to look at how to model behaviour in web components using state machines and statecharts in FicusJS.

State machines vs statecharts

What is the difference between state machines and statecharts? Understanding state machines is almost the same as understanding statecharts. Statecharts are the “bigger brother” of state machines and is essentially a state machine that allows any state to include more machines, in a hierarchical fashion.

Benefits of using state machines

There are many benefits to using state machines or statecharts, including:

  • The behaviour (when things happen) is decoupled from the actual component (what happens).
  • They produce lower bug counts than traditional code.
  • As the complexity of your program grows, statecharts scale well.
  • You explore all the possible states of your program.

Designing state machines

The first step in designing state machines is to use a whiteboard or pen and paper and write down the possible states of the component you want to create. This is an important part of the process and should involve UX designers, product owners and developers. Talking through the behaviour of the component and writing down all the states helps to identify all the possibilities up-front.

Once you have a list of identified states, create a visual state machine to check that the behaviour works. Using a visual tool helps all stakeholders to understand the state machine and the behaviour of the component.

An example

We are going to create a visual depiction of a simple state machine. It is a model of a simple on/off switch.

Image description

To view the visual model and interact with it visit https://stately.ai/viz/04fcf47e-df9f-4f78-b3b9-4826c24b2df9

The visual model is a simulation which helps to refine the behaviour through defining the states and transitions of the state machine.

  • It consists of two states, “on” and “off”. This machine can therefore be in exactly one of the two states at any point in time. In other words, the transitions between states are instantaneous.
  • The “flick” event causes it to transition between states.
  • When the machine enters the “on” state, a side effect occurs. A light is turned on.
  • When the machine exits the “on” state, another side effect occurs. A light is turned off.

The definition of the machine is as follows:

{
  id: "switch",
  initial: "off",
  states: {
    off: {
      on: {
        FLICK: {
          target: "on",
          actions: "turnLightOn",
        },
      },
    },
    on: {
      on: {
        FLICK: {
          target: "off",
          actions: "turnLightOff",
        },
      },
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

Creating a state machine

In order to create a state machine, you import the createMachine function and pass it a machine definition object.

FicusJS decorates the @xstate/fsm package to provide additional features like extended state getters. You can also use the full XState library with FicusJS, See the state machine docs for more information.

import { createMachine } from 'https://cdn.skypack.dev/@ficusjs/state@3/xstate-service'

const machine = createMachine({
  id: "switch",
  context: { bulb: 'off' },
  initial: "off",
  states: {
    off: {
      on: {
        FLICK: {
          target: "on",
          actions: "turnLightOn",
        },
      },
    },
    on: {
      on: {
        FLICK: {
          target: "off",
          actions: "turnLightOff",
        },
      },
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Creating side effects

Our state machine has two side effects. These are actions that are invoked when a transition is received.

Actions are passed as options to the createMachine function.

import { createMachine } from 'https://cdn.skypack.dev/@ficusjs/state@3/xstate-service'

const definition = {
  id: "switch",
  context: { bulb: 'off' },
  initial: "off",
  states: {
    off: {
      on: {
        FLICK: {
          target: "on",
          actions: "turnLightOn",
        },
      },
    },
    on: {
      on: {
        FLICK: {
          target: "off",
          actions: "turnLightOff",
        },
      },
    },
  },
}

const options = {
  actions: {
    turnLightOn: assign({
      bulb: 'on'
    }),
    turnLightOff: assign({
      bulb: 'off'
    })
  }
}

const machine = createMachine(definition, options)
Enter fullscreen mode Exit fullscreen mode

Interpreted state machines

We can use an interpreted state machine to keep track of the state for us. To create an interpreted state machine, pass the machine created with createMachine into the interpret function. The result of the interpret function is called a service (a running instance of the machine).

const machine = createMachine(definition, options)
const service = interpret(machine)
Enter fullscreen mode Exit fullscreen mode

Component usage

The service created with interpret can then be passed to a web component for it to react to state changes and therefore allow you to render UI based on the current state.

To pass the service to the component, use the withXStateService component extension.

import { html, renderer } from 'https://cdn.skypack.dev/@ficusjs/renderers@5/uhtml'
import { createCustomElement, withXStateService } from 'https://cdn.skypack.dev/ficusjs@5'

createCustomElement(
  'switch-state-machine',
  withXStateService(service, {
    renderer,
    onChange () {
      this.fsm.send('FLICK')
    },
    render () {
    }
  })
)
Enter fullscreen mode Exit fullscreen mode

The result

The following Codepen is an implementation example of the switch state machine used to turn on/off a light bulb.

Summary

State machines and statecharts reduce complexity, bugs and inconsistencies in UI development by helping you define components using a behaviour-driven model. This ensures that what is designed through the visual model, is the same output that components provide through the implementation of XState.

State machines are a great fit if you need to control the application’s flow carefully. If you start using XState, it does not mean that everything will have to live inside the state machine. Some things need to be controlled on a local level inside a component rather than on a global level inside a state machine. That goes for both the state and the side effects.

Make smaller state machines rather than one enormous state machine. Programming is about composability, making larger things from smaller things — and state machines compose well.

💖 💪 🙅 🚩
ducksoupdev
Matt Levy

Posted on July 20, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related