Rise of the finite-state machines

Adam Sawicki
6 min readNov 9, 2020

Recently I came across an interesting concept of how UI can be modeled and built — “Pure UI”, described by Guillermo Rauch on his blog: https://rauchg.com/2015/pure-ui. The key point of the “Pure UI” approach is that an application UI is a pure function of the application state. The application state changes — so should the user interface. It means that the interface fully communicates the application state to the user.

Possible states map

What it implies is that by discovering all possible UI states, we will have the whole picture of the application complexity. The states’ map will help in effort estimations and progress measurement.

Online shopping application diagram

An application can transition from one state to another via events fired by user interaction or runtime environment — interval events, requests’ responses, browser events, etc. An event is just a reason for the state update though, we need instruction on how the state actually changes. Let’s call an event with these instructions a transition:

event + instruction = transition

If we have Redux as an example, an action would be an event and a change instruction would be a reducer function. These two elements define a transition to a new state.

Transition to a new state in Redux

In this case, however, we don’t explicitly know in what state the application is in. We just apply changes to some of the store parameters which we think should be modified once an event occurs. It is easy to lose track of necessary parameters’ updates for new events added to our application. Wouldn’t state transitions be more resilient if we identified all the states upfront and defined all store parameters’ values for each of them? Having a map of the application states with allowed transitions between them will make software less vulnerable to developers' mistakes caused by overlooking a parameter that should have been updated.

Identifying possible states

Let’s find out how to identify possible states of an application. The vast majority of modern applications are in fact infinite state machines because they are written with Turing-complete programming languages.

An application that adds two numbers and displays the outcome is a good example. Potentially, there might be billions of different results. Representing them as separate states is not practical. Intuitively, it is reasonable to split quantitive state values from continuous ones. There is no straightforward recipe for that, we need to use our own judgment and experience to properly design the state.

An infinite number of results

A rule of thumb can be treating application business data as continuous state values. For example, user age is business data inputted by users themselves.
Information if the user is underaged is derived from their age and is quantitive (yes, no) It is a good candidate for a finite-state parameter if our fine whiskey rating application should prevent exposing its content to a younger audience.

Based on this information we can distinguish two states: content allowed and content blocked. They will find reasonable representation in the user interface. The first state will present a list of whiskeys to rate, the second one will display information that content is blocked. There is no need for representing any other user age range as a separate UI state.

Finite-state machines

Identifying application possible states and connecting them with transitions is not a new idea in computer science. There is actually a mathematical model called a finite-state machine. It is a computation model, an abstract machine that can be in exactly one of a finite number of states at any given time. A wide range of applications in automatic control systems prove the validity of the concept.

Turnstile is a finite-state machine

Finite state machines aren’t a silver bullet that will resolve all UI problems, however once used wisely, they can simplify initial development and speed up additional features implementation.

Let’s see what are their advantages. First of all, using finite state machines will allow us to describe business rules and user interactions in a declarative way, switching focus from coding to business requirements. There are visualization tools available that help turning state machine configurations into statechart diagrams, which improves communication between developers and product team members. Debugging also becomes easier — you can see a history of states and events that triggered the transitions. Finally, components can be leaner as a side effect of switching to a declarative coding style.

Xstate visualization tool

Modeling real-world problems

It is worth mentioning that modeling real-world problems with state machines takes time and practice to do well. All team members needed some time to learn the approach and get comfortable with it. Simple coding errors in state machine configuration often led to bugs that were hard to track down. Since there are no clear recipes on how to reduce complexity when your model grows, you have to do it yourself and it can be a bit painful.

I would recommend experimenting on a smaller scale first. Regardless of how sound the concept is, putting it into practice is not trivial and it takes both time and effort to be done the right way. Instead of going all the way and managing the full application state with state machines, my suggestion would be to first try it in the scope of a single component. That’s a nice isolated level you can use as a proof of concept without interfering with the rest of the application. If you find value in that, you just might be ready to drive more complex behaviors using state machines.

Drag and drop can be modeled as a finite-state machine

Let’s compare Redux, a widely popular state management library, with the finite state machines approach. Redux is essentially a state container where events (actions) are sent to a reducer that updates state and it does not dictate how you define your reducers. They are plain functions that return the next state given the current state and event (action). The finite state machine model helps to build a “reducer with rules” — you define legal transitions between states due to events, and also which actions should be executed in a transition (or on entry/exit from a state). Redux relies on the developer to manually prevent impossible states.

Summary

With that said, the finite-state machine is a powerful model for user interfaces. In general, it should be used if:

  • You need to manage state that can come from many different sources
  • The behavior of various parts of your app change over time and you want to clearly reason about what can/can’t happen without littering your app with if-statements everywhere
  • There are a lot of events and event-driven behaviors in your app
  • Your app can be represented as being in many different “modes”, or finite states

While building a simple CRUD app, it might not make sense to use a finite-state machine. However, the complexity of this kind of applications tends to grow while new requirements are coming in. The key is to monitor your application features and apply a finite-state machine model in places where the above points are valid.

--

--