This content originally appeared on Telerik Blogs and was authored by Leonardo Maldonado
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.
This is how a traffic light works:
- We have an initial state of green.
- We have a timer, and, after the timer hits 30 seconds, the state will change to yellow.
- Now that our state is yellow, after the timer hits 10 seconds, the state will change to red.
- In our red state, after the timer hits 30 seconds again, it will change our state 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.
- We have a finite number of states and an initial state.
- The state can only change (transition) in response to an input.
- After the state changes, it produces an 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:
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.
This content originally appeared on Telerik Blogs and was authored by Leonardo Maldonado
Leonardo Maldonado | Sciencx (2021-03-17T22:20:57+00:00) How to Use Finite State Machines in React. Retrieved from https://www.scien.cx/2021/03/17/how-to-use-finite-state-machines-in-react/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.