Model web component behaviour with XState
Matt Levy
Posted on July 20, 2022
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.
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",
},
},
},
},
}
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",
},
},
},
},
})
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)
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)
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 () {
}
})
)
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.
Posted on July 20, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.