This article is about React-Machinery, a library for creating and using state machines in react to control UI. Find the full source on github
State is everywhere, and it’s an especially hot topic in the web development community. We have libraries like redux to help manage global state, but on the component level we can go further and harness some good old fashioned computer science.
Almost all UI components have a specific set of states they can be in. Simple components like dropdowns have unselected, in selection, and selected states. Complex views like a youtube video component have many states, like playing, paused, loading — and many substates like if the volume control is expanded, or if closed captions are enabled. This is not limited to UI either— any kind of application, object or function can be stateful.
As developers we often model these states using boolean variables and if statements. While this is not bad per se, there are some problems with this approach:
- combinations of boolean expressions can be tricky to understand and communicate
- as requirements change, these branching if statements can start to produce possibly unwanted side effects
Ever had one of those bugs where for some reason, two completely different parts of state come together in an unexpected way to produce a weird effect? I’ve had this on almost every kind of project I’ve worked on, from games to SPAs to REST backends.
Fortunately computer science provides a very nice solution to this class of problems, aptly named Finite State Machines.
Finite State Machines > Combinational Logic
First of all, roughly speaking a finite state machine is:
A finite-state machine (FSM) or finite-state automaton (FSA, plural: automata), finite automaton, or simply a state machine, is a mathematical model of computation. It is an abstract machine that can be in exactly one of a finite number of states at any given time. The FSM can change from one state to another in response to some external inputs; the change from one state to another is called a transition. An FSM is defined by a list of its states, its initial state, and the conditions for each transition.
Hopefully that definition connects with what I talked about above with UI states. If not, don’t worry — a practical example is coming!
As a small note, it’s interesting to take note of that word automaton in the description. In math and computer science, an automaton is an “abstract machine” which deals with computability. These automata form a hierarchy:
As you can see from the diagram, finite state machines occupy a higher tier of automata than mere combinational logic (booleans and if statements). Simply put, they provide you the language to describe something stateful in a more precise way than you can with combinational logic alone.
We can put this theory into practice and use them with React ⚛️
The <HardChoiceButton> Component
A while back Wes Bos posted this tweet shortly after the Hawaiian missile scare:
I’m going to use this component as inspiration and build something similar using react-machinery.
The result will be a component called the HardChoiceButton, and it will take a message to display, a number of seconds to countdown, and an action function to run as props.
The button starts in a wait for input state, and when a user presses the button, they enter the pending state.
During the pending state a timer is ticking down from 5 (or whatever value we pass). At any time during this countdown, the user can press the button again to abort the operation, taking them to the aborted state.
If they don’t press the button during pending, and the countdown makes it all the way to 0, then the action is executed and the button goes to the done state.
We can represent the above using logic with the following state diagram:
Bringing in react-machinery
Let’s start defining the HardChoiceButton class:
As you can see, the StateMachine component exposed by react-machinery take 4 props: the states array, functions to get/set the current state, and some data properties which I’ll get to in a second. You can wire up whatever data source you want to the getCurrentState() and setNewState() functions — taking data from a redux store or mobx state tree — but this example just uses regular component state.
The states array contains objects that have a name, and in this case a function called render. The render function here represents the component to render when in the wait-for-input state. This function receives the data object as props, which allows us to display the ‘Launch Missiles’ message.
The HardChoiceButton is not much good if it only has a single state though! Let’s go ahead and fill in the rest of the states.
A small note before we do: components that are associated with each state can be given in two ways. The first is through a render function like above. This way follows the render prop pattern. The second way is to instead specify a component property. This is not a function, but rather just a normal component. This style is a little more applicable if you have a react class component with it’s own internal state.
Complete state definition
When we click the button in the wait-for-input state, we call transitionTo(‘pending’). This is a special function that the StateMachine component passes to render along with the data object.
There is another key inside called validTransitions. This is used to register the states that are allowed to be used with transitionTo. Trying to transition to a state not registered will result in an error.
There is also a beforeRender function that allows you to perform some side-effecty stuff — in this case making sure the timer is reset whenever we enter the wait-for-input state. This way we can ensure the timer always starts at 5.
When the pending state becomes active, it renders the Pending component, a fairly straightforward class component that creates and destroys and interval, and renders the new button. Every second, the interval calls the decreaseTimeLeft() function that was defined in the data prop of StateMachine.
The mechanism to transition state to the done state is a little different for pending though. Instead of calling transitionTo, we can describe an automatic transition — one that uses a function to determine whether to change state. These kind of transitions can be defined in states by the autoTransitions key.
The rest of the states just use combinations of these different kinds of transition logic.
So what benefits can you expect modelling components this this way? For starters, you know for sure that you cannot arrive in a state that you did not explicitly define. Think about that for a minute because it’s a pretty big idea. Having a state machine like this essentially eliminates a whole class of bugs that you have to test for.
Building on top of that, because you’re explicitly defining this data structure, you can use it to generate information about your components. Take the following function:
That produces the following object (shown as JSON):
You can use this information to generate state diagrams for your component. It’s literally self documenting.
Now imagine you have logging enabled on a production site. You can easily write a state machine that logs every state change, providing you with breadcrumbs back to the source of any error.
Lastly, changes to requirements in business logic can often be cleanly added. Let’s say we needed to add a new behaviour when the user clicks the button with 1 second left on the clock. Instead of simply going to the aborted state, we need to go to the that-was-close state.
If you find yourself developing bugs because of strange combinations of if-statements, considering implementing components using finite state machines instead. You can start using them in React right now with react-machinery (only 2kb gzipped!). State machines will help you model, reason about, and communicate the workings of your components more effectively.