ReactT2 Dark_1200x303

Learn about finite state machines, the advantages of this computer science concept and how we can use it in React apps.

Dealing with state logic is always painful. That’s why we’re always restructuring and making sure that the functions that update our state in React are working correctly.

The goal of every React developer when starting an application is, for sure, to create up-to-date state functions that don’t cause unexpected side effects in our application. But we know that it still happens a lot.

Every day our application grows in size—components are getting bigger, we need to make more API calls, so we need to create more state to handle all of that data. That’s where we get trapped and start to create side effects and unexpected bugs. Handling all that state logic data in a simple, powerful, consistent way, while avoiding side effects and bugs is a challenge that we face daily.

Finite state machines might be the right choice for you right now, to solve unexpected side effects and maintain your application bug-free for a long time. To start with finite state machines in React, let’s first understand how they work, their purpose and why they are one of the most powerful ways to handle state logic in an application.

What Are Finite State Machines?

Whether or not you’ve heard the term “finite state machines,” we’ve been using them for a long time, and not only in computation—in real life too.

The most common finite state machine example that we can use is a traffic light. A traffic light has only three states: green, yellow and red.

Traffic light states: green, yellow, red

This is how a traffic light works:

  1. We have an initial state of green.
  2. We have a timer, and, after the timer hits 30 seconds, the state will change to yellow.
  3. Now that our state is yellow, after the timer hits 10 seconds, the state will change to red.
  4. In our red state, after the timer hits 30 seconds again, it will change our state to green.

Traffic flow of states: green points to yellow, which points to red, which points to green.

Very simple. We have a finite number of states (green, yellow and red), which means that we only have three possible states. There isn’t another state possibility.

To change for another state, we need input. In our traffic light example, the input is our timer. Whenever the timer hits a specific amount of seconds, it transitions to another state. This new state is our output.

That’s basically how a finite state machine works.

  1. We have a finite number of states and an initial state.
  2. The state can only change (transition) in response to an input.
  3. After the state changes, it produces an output.

Input → State → Output

With a very simple example, we can understand how finite state machines work. Now, take a look at your code. I’m pretty sure that you can identify a few small finite machines in your code very easily.

Why Use Finite State Machines?

You might be wondering what the benefits of a finite state machine are, why you should use it to handle complex state logic. I’ll list a few advantages:

  • A finite number of states. If you have a finite number of states, you already know how your state logic is going to look and when should you change from one state to another.
  • Visualized modeling. With finite state machines you can use a state machine visualization tool to create your state machine and visualize how your state logic will look. Also it gets easier to identify errors or when you’re changing to a wrong state.
  • Avoid unexpected side effects . This is one of the most powerful advantages of finite state machines. It’s relative to the first point, but with a finite number of states, you’ll drastically reduce the number of unexpected side effects that you create in your state logic.
  • Relatively easy to debug. Debugging a finite state machine is relatively easy. You can use a state machine visualization tool for that, and it’ll save you a few hours when you’re debugging.
  • Strong test coverage. With a finite number of states, it gets pretty easy to write tests for your state logic. If you don’t know how and to where your state is going to change, you can avoid a lot of useless tests and remove those side effect tests that we usually write.

Finite State Machines vs Statecharts

Statecharts were invented by David Harel, and they are an extension of state machines. Statecharts are more scalable and consistent than simple state machines, and they come with some expensive features to help more complex systems.

One of the main features of statecharts is that they have a hierarchy state and each state can have substates. In a statechart, a state that has no substate is called an atomic state. A state that has a substate is called a compound state. Other nice features that statecharts have are actions, guards, multiple transitions and state history.

So, when you see someone else talking about statecharts, don’t get confused—they’re just an extension of a finite state machine with a few extra powerful features.

Now that we know about state machines and how they work, let’s find out how we can use them in our React apps.

XState

XState is a JavaScript/TypeScript library to create finite state machines and statecharts. This library is, by far, the best option nowadays to start to working with finite state machines and statecharts in our apps. In this tutorial, we’re going to work with XState for React, but this library also has a package for Vue.

So, let’s get started with XState and learn how we can create our first finite state machine and achieve a better level of state logic in our apps.

XState has a visualizer that helps us create our finite state machines. We can use this visualizer to see how our finite state machine is working and if we have any errors. So, let’s use this visualizer to have a better understanding of how XState works.

Creating our First Finite State Machine

To create a finite state machine using XState, we should use the Machine object. Inside this object is where we’re going to create all the transitions and events for our finite state machine.

Let’s name this machine lightMachine and use the Machine object:

const lightMachine = Machine({
  ...
});

Each Machine should have an id and an initial state. We’re going to give the id of lightMachine, and the initial state of our traffic light state machine will be green.

const lightMachine = Machine({
 id: 'lightMachine',
 initial: 'green'
});

States

Our state is basically a representation of our system: As the events occur in our applications, the state changes. A finite state machine can only be in one state at a given time; it’s impossible to be in more than one.

In a traffic light, we can think in only three possible states: green, yellow and red. Inside our Machine object, we define our state using a property called states, which is also an object. So, let’s create our first states.

const lightMachine = Machine({
 id: 'lightMachine',
 initial: 'green',
 states: {
   green: {},
   yellow: {},
   red: {}
 }
});

For now, our finite state machine is doing basically nothing. Inside each state, we’re going to use a property called on. This property will change our state when a transition occurs.

This is how it works: We give a name to the transition and the final state that we want. So, for example, we want to give the name of YELLOW to our transition, and we want to go to the yellow state.

Let’s do the same for the other states, but we’re going to change the final state, and follow the same behavior of a traffic light. From green to yellow, from yellow to red, from red to green.

const lightMachine = Machine({
 id: 'lightMachine',
 initial: 'green',
 states: {
   green: {
     on: {
       YELLOW: 'yellow'
     }
   },
   yellow: {
     on: {
       RED: 'red'
     }
   },
   red: {
     on: {
       GREEN: 'green'
     }
   }
 }
});

In our visualizer, this is how our finite state machine is looking:

The visualization of our finite state machine

By clicking in our transitions, we can see our state changing, and our finite state machine is working as expected. One state at a time, without any errors.

Context

In XState, we have something called Context. Context can be defined as “quantitative data”. We can understand it like strings, functions, objects, etc. So, let’s create our context to understand how it works.

Inside our Machine object, below the initial property, we’re going to create an object called context.

context: {
 updated: 0
},

Now, every time we change our state, we’re going to increment that context by 1. But how we can do that? Well, in XState, we have something called Actions. With Actions we can easily dispatch side effects.

Actions

So, we’re going to create a function called updateAction, and use the assign function to update our context.

const updatedAction = assign({
 updated: (context, event) => context.updated + 1
})

Also, we’re going to change a few things inside our Machine object now. Inside each state, we’re going to change to something like this:

green: {
     on: {
       yellow: {
         target: 'yellow',
         actions: 'updatedAction'
       }
     }
   },
   yellow: {
     on: {
       red: {
         target: 'red',
         actions: 'updatedAction'
       }
     }
   },
   red: {
     on: {
       GREEN: {
         target: 'green',
         actions: 'updatedAction'
       }
     }
   }

When we have actions to dispatch, we need to change our events to an object and have two properties: target is the next state, and actions are the actions that we’re going to dispatch.

const updatedAction = assign({
 updated: (context, event) => context.updated + 1
})
const lightMachine = Machine({
 id: 'lightMachine',
 initial: 'green',
 context: {
   updated: 0
 },
 states: {
   green: {
     on: {
       YELLOW: {
         target: 'yellow',
         actions: 'updatedAction'
       }
     }
   },
   yellow: {
     on: {
       RED: {
         target: 'red',
         actions: 'updatedAction'
       }
     }
   },
   red: {
     on: {
       GREEN: {
         target: 'green',
         actions: 'updatedAction'
       }
     }
   }
 }
});

Usage in React

We now have our finite state machine working fine, so let’s get started using it in React and see how it works. First, let’s install some packages:

yarn add xstate @xstate/react

Now, we should import the Machine object from xstate and the useMachine hook from @xstate/react.

import { Machine } from "xstate";
import { useMachine } from "@xstate/react";

Inside our component, we’re going to paste the finite state machine that we created using the visualizer, and also use the useMachine hook.

The useMachine hook is pretty similar to the other hooks of React. The returned state is current, and the send function is to update our state using our actions. We’re going to put the useMachine that we created as value, and also create a new object. Inside this new object we’re going to create a property called actions and put our updatedAction action there.

const [current, send] = useMachine(lightMachine, {
 actions: { updatedAction }
});

Inside our current state, we have a lot of different properties. For now, we’re going to use context and matches. With the context property, we’ll be able to get our context, and the matches property is a function to check if our finite state machine is in that specific state.

So, we’ll create a title to display how many times our state was updated, and also create three div elements using the matches property to display content. We’re going to compare each div element to each state, so we’ll only display the div of that respective element.

return (
<div>
<h1>Light traffic</h1>
<h1>Updated: {current.context.updated} times</h1>
{current.matches('green') ? (
<div style={{ width: 60, height: 60, borderRadius: "50%", background: "green", marginTop: 10 }} />
): null}
{current.matches('yellow') ? (
<div style={{ width: 60, height: 60, borderRadius: "50%", background: "yellow", marginTop: 10 }} />
): null}
{current.matches('red') ? (
<div style={{ width: 60, height: 60, borderRadius: "50%", background: "red", marginTop: 10 }} />
): null}
</div>
);

Now, we’re going to create three buttons. Each button will change the state for a specific target. To change the state, we’ll use the send function from our useMachine hook. If the button doesn’t match the state that we want, the button will be disabled.

So, for example, we know that our first state is green, and after that, we go to yellow. So our first button will have the name of Yellow, but it will be disabled if it doesn’t match the state of green. To change our state, we’ll simply put an onClick method and use the send function, passing the next target which is YELLOW.

<button
 disabled={!current.matches('green')}
 onClick={() => send('YELLOW')}>
  YELLOW
</button>

Very simple. Now we’ll do that for the other two states, and our final component will look like this:

const Light = () => {
const lightMachine = Machine({
 id: 'lightMachine',
 initial: 'green',
 context: {
   updated: 0
 },
 states: {
   green: {
     on: {
       yellow: {
         target: 'yellow',
         actions: 'updatedAction'
       }
     }
   },
   yellow: {
     on: {
       red: {
         target: 'red',
         actions: 'updatedAction'
       }
     }
   },
   red: {
     on: {
       GREEN: {
         target: 'green',
         actions: 'updatedAction'
       }
     }
   }
 }
});
const updatedAction: any = assign({
 updated: (context: any, event: any) => context.updated + 1
})
const [current, send] = useMachine(lightMachine, {
 actions: { updatedAction }
});
return (
<div>
<h1>Light traffic</h1>
<h1>Updated: {current.context.updated} times</h1>
{current.matches('green') ? (
<div style={{ width: 60, height: 60, borderRadius: "50%", background: "green", marginTop: 10 }} />
): null}
{current.matches('yellow') ? (
<div style={{ width: 60, height: 60, borderRadius: "50%", background: "yellow", marginTop: 10 }} />
): null}
{current.matches('red') ? (
<div style={{ width: 60, height: 60, borderRadius: "50%", background: "red", marginTop: 10 }} />
): null}
<button disabled={!current.matches('green')} onClick={() => send('YELLOW')}>YELLOW</button>
<button disabled={!current.matches('yellow')} onClick={() => send('RED')}>RED</button>
<button disabled={!current.matches('red')} onClick={() => send('GREEN')}>GREEN</button>
</div>
);
};

We now have a traffic light application working using XState. That’s very awesome. We can see that our logic is bug-free, since we’re not able to be in more than one state at a time.

XState and finite state machines make a lot of sense to create better applications when you’ll have a lot of different states. Maybe it’ll take some time to grasp the concepts of this powerful library, but in the long term, it’ll help you to write better state logic.

Conclusion

In this article, we learned more about a very important concept of computer science known as finite state machines.

We learned how state machines work, the advantages that finite state machines have over the common state management that we’re used to working with, and the differences between finite state machines and statecharts.

We also learned how we can work with finite state machines in React apps using XState, a JavaScript/TypeScript library that allows us to create finite state machines and have a better app, creating a more consistent state and bug-free logic.


Leonardo Maldonado
About the Author

Leonardo Maldonado

Leonardo is a Full Stack Developer, working with everything React-related, and loves to write about React and GraphQL to help developers. He also created the 33 JavaScript Concepts.

Related Posts

Comments

Comments are disabled in preview mode.