Statecharts in User Interfaces

Statecharts are a good fit for solving certain problems with coding user interfaces. Both statecharts and most UI technologies are event driven, but the technologies complement one another very well.

Event Action paradigm

Most UIs are event driven, meaning that the user interface components themselves generate events when the user interacts with them. These events typically trigger actions attached directly to the UI components. In some systems, the events are passed around things like component hierarchies, or forwarded to an event loop that gets to decide what action to perform. The implementation of the UI component somehow decides what to do, what to show, what to stop doing and what to stop showing.

This is called the Event Action paradigm, because the action is tightly coupled to the event.

Statecharts inject themselves between the event being generated and the action being performed. A statechart’s main function is to take an event created somewhere, and make a decision on doing stuff. It does precisely what most event driven UIs don’t do, or provide a framework for.

A comparison

In a traditional event driven user interface component, say, the ubiquitous HTML <input> element, the UI component generates a lot of events, that the developer can subscribe to, from gaining or losing focus, editing, selecting, mouse movement and so on. A developer has a plethora of events to choose between. There is a similar almost infinite set of things that can be done to a user interface. Each element has a wide range of possible mutations. It is up to the developer to decide what to do based on whatever event that happens.

In simple user interfaces, the event-action paradigm is fine. Here, for example is an input handler that turns the text green when text is entered into the field:

.green {
    box-shadow: 0 0 30px green
}
<input id="my_editor">
var field = document.getElementById("my_editor");
function handleChange(e) {
  field.classList.add("green")
}
field.onchange = handleChange

Don’t cringe, it’s a simple component that turns green (an action) whenever a particular event happens (the field is modified). This is the component’s behaviour.

However, in order for this code to support anything other than turning it green when it’s modified, it will need conditional logic. If the field should only turn green if the has any text, then the event handler needs an if test, and it needs to check the “current state” of the component. This is the beginning of the complexity creep.

function handleChange(e) {
  if (field.value != "") {
    field.classList.add("green")
  }
}

Statecharts

As mentioned earlier, when implementing statecharts, the events are passed on to the statechart instead of being acted upon directly. The statechart then determines what to do.

The simplest state machine (a simple form of a statechart) is shown below. It has the same behaviour as the example above:

var currentState = "not_green";
var stateMachine = {
  "not_green": () => field.classList.add("green")
}
function handleChange(e) {
  stateMachine[currentState]()
}

Now this state machine is extremely simple, it only has one state and knows only one way to behave. But it is now a lot easier to make this component do more.

var stateMachine = {
  "not_green": () => { field.classList.add("green"); currentState = "green"; },
  "green": () => { field.classList.remove("green"); currentState = "not_green"; }
}

By changing the state machine alone, we can now change the behaviour of the component. It now alternates between having the “green” class every time the field changes.

We can introduce a guard in order to prevent the event from having an effect. Let’s extend it so that the field is green only when text has been added to it:

var stateMachine = {
  "not_green": () => {
    if (field.value == "") return;
    currentState = "green";
    field.classList.add("green");
  },
  "green": () => {
    if (field.value != "") return;
    currentState = "not_green";
    field.classList.remove("green");
  }
}

See the Pen Green input box by Erik Mogensen (@mogsie) on CodePen.

This is a crude approximation of a state machine, but it is a state machine, and has a lot of the moving parts of a statechart too:

It has a few limitations too, though

While it is possible to “roll your own” state machine, it is likely not worth the effort. There are numerous edge cases that you need to consider, and if you’re not careful, such a one-off state machine becomes more difficult to maintain than the original spaghetti code it was meant to displace. It is a bit like rolling your own date handling code; it works for the simplest of cases, but is quickly outgrown.

The code shown above was introduced solely to describe how a state machine fits in, in the context of user interfaces. In order to show more advanced examples, it’s necessary to avoid coding the inner workings of the state machine, and try to focus on the important parts, namely the different states, and which events causes the states to change.

In order to help, we’ll be introducing a statechart library

Introducing XState

XState is a javascript library that essentially allows us to hide the inner workings of the state machine. You provide it with an object that describes the state machine you’re interested in, and XState returns a state machine that provides pure functions that you can use to answer the question: “If I’m in this state, and this happens, what should I do?”

To give you a quick primer, we’ll rewrite the green / not green state machine above using XState.

First of all, we’ll need a state machine, constructed by XState.

import { Machine } from 'xstate';
const stateMachine = Machine({
  id: "example",
  initial: "not_green",
  states: {
    "not_green": {
      on: {
        "change": "green"
      }
    },
    "green": {
      on: {
        "change": "not_green"
      }
    }
  }
}

One thing to note is that in state machines, and statecharts, events are given explicit names. For our simple example I called the event change.

Now, this stateMachine variable provides a pure functional interface to the state machine. This means that this state machine cannot and will not have side effects. Every time you use it, you tell it what the “current” state is, the event (what “happens”), and it tells you what happened.

We start off our state machine by asking the state machine what the “initial” state is:

var currentState = stateMachine.initialState;

currentState is an XState State instance, which has a value which initially should be "not_green". It represents our state.

You can then simulate what happens if you pass it the change event:

currentState = statemachine.transition(currentState, "change");

currentState will now describe the green state, and if you did it again, it would be the not_green state once again. What we have might seem like a pretty advanced boolean, but don’t despair. It’s time to hook this state machine up to our user interface. To start with, we’ll let any change in the text field will trigger the “change” event.

The first thing we need to do when handling the event is to trigger a state change, as shown above. The next thing is then to check which state we’re now in, and respond accordingly.

if (currentState.value == "green") {
  field.classList.add("green");
}
if (currentState.value == "not_green") {
  field.classList.remove("green");
}

See the Pen Green input box (XState version 1) by Erik Mogensen (@mogsie) on CodePen.

Guards

This initial implementation switches between the green and not_green states every time a change happens. We wanted to switch the state depending on the value of the text field. Let’s do that.

If you’re not used to thinking in state machines, it is usual to try to solve this outside the state machine, by perhaps checking the value before telling the state machine about the event. However state machines have support for something called guards which allow the state machine to make the decision. Allowing this to be dealt with inside the state machine ends up being more flexible.

First of all, we need to gather information about the world that we want the state machine to be able to inspect, a form of “extended state”. For our example we want the state machine’s behaviour to depend on the input value, or more specifically, the length of the value (or something else that constitutes validity). This extra data is passed as the third parameter to transition:

currentState = stateMachine.transition(currentState, 'change', field.value);

For simplicity, we’re just passing the value of the field as the extended state, we could pass in the length or, even better, an object literal with room for more variables.

In the state machine definition, we now change the on: { change: ... } handlers so that they check the guard before continuing. When we’re in the not_green state we will only go to green if the length is greater than 0:

on: {
  change: {
    target: "green",
    cond: text => text.length > 0;
  }
}

Here cond is short for condition. The condition must hold (evaluate to true) for the transition to happen. Conversely, we’ll check that when we’re in the green state we’ll only transition to the other state if length is equal to 0.

See the Pen Green input box (XState version 2, with guards) by Erik Mogensen (@mogsie) on CodePen.

TKTK update the code samples from the pen:

Actions based on the “current state”

The state machine as it stands is useful in its own right: the behaviour is isolated in the statechart definition, and we can make some changes. However, code outside the state machine is completely dependent on the names of the states, so introducing a new state would require us to change the code that talks with the state machine.

There’s one last thing that we ought to do, and that’s implement actual side effects of a state machine.

When you have a state machine or statechart that “drives” your UI, it is quite common for the states in the statechart to (at least at the highest level) correspond to “modes” of the user interface. In our sample we have “green” and “not_green” as states, and we have an ugly if test which checks which state we’re in, and performs some actions based on it (adds/removes a class).

An easy simplification of this is to set the class of the element to the value of the state and be done with it. This is a common way of using the “current state” to effect changes to the user interface. That ugly set of if-tests can be reduced to a simple assignment:

field.classList.value = currentState.value;

The field now gets the class based on the current state of the state machine. If we introduce a new state in the state machine, it automatically becomes a class of the field, for better or worse.

This has some nice benefits:

“Actual” side effects

There are some side effects that cannot be done based on the class alone, such as making a HTTP request. Such long running things are in statechart terminology called “activities”, and activities are started and stopped by way of actions. Let’s make an activity that represents a HTTP request. That activity is started and stopped by way of two functions we’ll define: startHttpRequest and cancelHttpRequest.

To avoid having to talk to a real server, we’re just going to use setTimeout to simulate a long running request:

var timeout = undefined;

startHttpRequest() {
  timeout = setTimeout(function() {
    timeout = undefined;
    resultsArrived({fake: "data"});
  }, 2000);
}

cancelHttpRequest() {
  if (timeout != undefined) {
    clearTimeout(timeout);
    timeout = undefined;
  }
}

function resultsArrived(data) {
  // interesting stuff happens here
}

TKTK update from pens

When startHttpRequest is called, it will call resultsArrived after 2 seconds, unless cancelHttpRequest is called first. It has an unfortunate, but deliberate behaviour that if you call the startHttpRequest many times, it will actually call resultsArrived many times.

Making use of our “HTTP client”

The point of this exercise is to show how side effects are handled in the state machine. As mentioned earlier, the HTTP request is called an activity and it is often the case that an activity is tied directly to being in a state. But instead of having an if test to check if we’re in a particular state, such side effects are often better to make explicit in the state machine. This is done by defining actions.

An action can happen as a consequence of entering or exiting any state. So if we want this HTTP request activity to happen in a particular state, we just specify entry and exit handlers to start and stop the activity in question:

green: {
  onEntry: "startHttpRequest",
  onExit: "cancelHttpRequest",
  ...
}

This declares that the startHttpReqest side effect should happen when the green state is entered, and that cancelHttpRequest should happen when it is exited.

XState then provides these actions as a string array in our currentState. If we happen to enter the green state, currentState.actions will be [ "startHttpRequest" ]. We can harness this by calling the corresponding function:

// TKTK should be handleEvent or something.
function transition(event, data) {
  currentState = stateMachine.transition(currentState, event, data);
  field.classList.value = currentState.value;
  currentState.actions.forEach(item => window[item]());
}

Forgive the carelessness of polluting the global scope, this is only to keep it as simple as possible.

The line currentState.actions.forEach(item => window[item]()) will take any string in the currentState.actions array and assume that it’s a global function, and simply call it.

The result is now that whenever we enter the green state, the startHttpRequest is called, and when we exit the green state, it calls the cancelHttpRequest.

Handling the results

We now have a situation where the HTTP request fires, and then two seconds later, we get some results. The resultsArrived function is called with some data.

In a non-statechart driven system, this resultsArrived function would typically immediately update the DOM and go about its business. However, in a statechart driven system, we get the function to only tell the state machine about the fact that some data arrived. The state machine would then decide what to do. This is a different event than typing text into a field, so we’ll give it a new name. We’ll call this the results event.

var results;
function resultsArrived(data) {
  results = data;
  currentState = stateMachine.transition(currentState, "results");
  field.classList.value = currentState.value;
  currentState.actions.forEach(item => window[item]());
}

We now already have some code duplication in that we have two places where the field.classList is updated, so it’s probably about time to extract this into its own function, a “stateful” wrapper around the XState state machine:

var currentState;
function transition(event, data) {
  currentState = stateMachine.transition(currentState, event, data);
  field.classList.value = currentState.value;
  currentState.actions.forEach(item => window[item]());
}

Now when results arrive, and the change event happens, we can instead call the transition function to deal with the state machine:

function handleChange(e) {
  transition("change", field.value);
}

function resultsArrived(data) {
  results = data;
  transition("results");
}

Note that the state machine hasn’t declared what should happen when the results event happens, so let’s add that to the definition too:

on: {
   results: "not_green",
   change: ...

This tiny change handles the results event by telling the machine to go to the not_green state. With our fake HTTP request, you can see that the machine leaves the “green” state after 2 seconds.

See the Pen Green input box (xstate version 4, actual side effects) by Erik Mogensen (@mogsie) on CodePen.

Building blocks

We now have a lot of the building blocks in order to make efficient use of statecharts. We have a machine which:

But harnessing these building blocks is for another page, but to start you off with some exercises, you can try a few things:

TKTK stuff below this line can be ignored, I think it’s food for a separate article.

Especially in declarative UI frameworks like HTML or React, it makes a lot of sense to model the statechart based on different “modes” of the UI, and use normal statechart mechanisms to control which is the “current state”. It therefore makes a lot of sense to re-use the “current state” and pass this knowledge on to the declarative UI, basically asking the UI to render the “current state” UI.

This has the benefit of keeping the statechart very much in line with the major modes of the UI. When a component gets a new “mode of operation”, it also gets a new “top level state”. This makes it easier when showing e.g. a statechart to non-developers, like QA or designers, since they will quickly recognise the states and be able to relate to them.

TKTK not finished yet.