Finite State Machines In React JS Using xstate

Sooraj Nair
Sooraj Nair, Software Engineer - Frontend
Finite State Machines In React JS Using xstate

What’s a finite state machine? To understand what a finite state machine is we need to first know what a state machine is.

A state machine is a machine that can transition into different states on passing certain inputs to them. Now add finite states to the machine so that your machine has only a limited number of outputs. You define a finite number of inputs and outputs and your machine can only transition to one of those outputs.

“Machine goes here or there, but nowhere else”

State machines are very useful since they never go out of bounds. Whatever the input is, if the machine recognizes it as feasible, then it will transition to a proper state, else based on your configuration, your state machine will either stop transitioning or throw an error.

For this article, let’s use xstate a state machine interpreter package that can be used with React JS with quite an ease.

We will create a simple authentication module that will have the following,

  • Login
  • Home
  • Server (we will use an express server for now)

For this project we will use npm to install all our packages.

We will first create our React App using create-react-app finite-machine-auth.

This will create our React App. Let’s modify it a little bit. We will create 2 Containers Login.js, Home.js and an Authenticator.js for authenticating Home page.

The main reason we are using state machines for our state transitions is that we want all our business logic to be on one side and all our UI on the other side. In other words, our state machine will take care of all the state transitions needed for authentication while React will render based on this transitioning state. So the code would look much more compact and debugging would be so much easier.

Developers when they hear “Debugging is easier”

Now let’s look at our finite machine

import { Machine } from "xstate";
export const authMachine:Machine(
{
id: "authentication",
initial: "unauthorized",
context: {
newLink: null,
errorMessage: null,
},
states: {
unauthorized: {
on: {
LOGIN: "loading",
},
},
loading: {
on: {
LOGIN_SUCCESS: {
target: "authorized",
actions: ["onSuccess"],
},
LOGIN_ERROR: {
target: "unauthorized",
actions: ["onError"],
},
},
},
authorized: {
on: {
LOGOUT: "unauthorized",
},
},
},
},
{
actions: {
onSuccess: (context, event) => {
if (event.reverse) {
context.newLink: "/";
} else {
context.newLink:null;
}
context.errorMessage:null;
},
onError: (context, event) => {
if (event.reverse) {
context.newLink:null;
} else {
context.newLink: "/login";
}
context.errorMessage:event.errorMessage;
},
},
}
);

That’s it all our state logic in a single machine. xstate gives us a Machine method to actually create a machine from an object configuration. Now let’s take a look at the machine itself deeply.

  • id - any string that can be used to identify a machine. Suppose we have more than 1 machine , we will use the id to find the machine.
  • initial - initial value of the machine.
  • context - context can be used to store anything in the machine and then pass it to the Components that use the machine.
  • states - the states where the machines can transition to. The machine will never transition to any other state regardless of the input. Poof!, easy debugging!
    • Each state has an on state, that gets executed whenever the machine gets is in that state and the corresponding input is passed.
    • Let’s consider our code as an example. Suppose our machine has its state in unauthorized, and we pass an input LOGIN to the machine. Now the machine knows that on{ LOGIN: 'loading'}. So the machine will now transition to the loading state. Any other input passed during the unauthorized state will not transition the machine, making it secure. The machine will either stop because it doesn’t know the transition or will throw an error if the configuration setting strict: true is used.

When you pass an input the machine doesn’t know

  • Now you can also have actions that the machine needs to perform when the machine is in a certain state. That’s where actions come into play.
  • You can call your actions in the on method on passing a certain input.
  • You can define your actions as a different object after the states. In this example, I have created two actions, onSuccess and onError. And I have called the action from the on method. I have used something called a target because the value of the input is more than one parameter.
  • The action functions will take two parameters context, event. So now the function can directly change the value of the context. The event object contains the value passed from the components.

For now, we will just use this configuration. In later discussions, we will use other configurations like guards, activities, parallel states, etc.

Before we start implementing our machine in our component, we can have a visual glimpse of our machine in action. Navigate to https://xstate.js.org/viz/ and just copy-paste our state machine code over the default code.

Let’s try it out. copy-paste our code and click Update. Now your machine would be visualized on the left.

Now let’s try our machine. Go to the EVENTS tab, and enter the type as LOGIN and click Send. You are now telling the machine to take the input LOGIN and change its state based on the input. The STATE tab will show you the current state value and context of the machine. Let’s send LOGIN input to our machine.

And it’s a success. Now let’s see if our functions work as intended. We will use LOGIN_SUCCESS for this test.

And it’s a success again. We see that our action gets executed successfully and our context changes.

Approved!!

So our machine seems to work as we intended it to work. Now we can start implementing with the application.

Now let’s take a look at our Authenticator.js

import React from "react";
import { Redirect } from "react-router-dom";
import { interpret } from "xstate";
import { authMachine } from "../Automata/authMachine";
import { authenticate } from "../Models/Auth";
import Loader from "../Components/Shared/Loader";
export default class Authenticator extends React.Component {
constructor(props) {
super(props);
this.state:{
auth: authMachine.initialState,
};
}
// For every transition set the state of the machine as current state
service:interpret(authMachine).onTransition((current) => this.setState({ auth: current }));
// Start the service when the component is mounted
componentDidMount() {
this.service.start();
this.authenticate();
}
// Stop the service when the component is unmounted
componentWillUnmount() {
this.service.stop();
}
// Authenticator function
authenticate:() => {
const { send }:this.service;
var { reverse }:this.props;
send("LOGIN");
authenticate()
.then((response) => {
send({ type: "LOGIN_SUCCESS", reverse });
})
.catch((err) => {
send({ type: "LOGIN_ERROR", reverse, errorMessage: err.toString() });
});
};
render() {
let { auth }:this.state;
if (auth.context.newLink) {
return (
<Redirect
to={{
pathname: auth.context.newLink,
}}
/>
);
}
if (auth.value === "loading") return <Loader />;
return this.props.children;
}
}

Now the API functions are just normal functions that communicate with the dummy express server that sends a jwt token for an authenticated user.

To use our state machine, we need to import our state machine to the component that we are going to use it in. In order for our state machine to work as intended, we need an interpreter to make out what the machine is doing.

Fortunately, xstate provides the interpret method for our ready use. We will use interpret to create a user-defined method called service. In this method, we will pass the machine to be interpreted to the interpret method. Then we will use the onTransition property of the machine to get the current state of the machine each time the machine changes its state.

Now we will start the service when the Component mounts and stop the service when the Component unmounts.

Now we have the machine setup completely. The only thing left for us to do is to communicate with the machine by sending inputs.

Our service method gives us a method called Send which allows us to send inputs to our machine. Send has multiple parameters, the first one being the input and the rest being the values sent to the machine.

Just run your code and you are done. Your Authenticator now has its state transitioned by the state machine. There you go.