The One where we started using State machines (XState) part 1

Akshay Pilankar
7 min readApr 22, 2022

React applications are built using components and their states. Let’s think of the state as a representation of the part of an application that changes.

When the application is small, components manage their state internally and it works well for applications with few components, but as we scale and our application becomes more complex, the complexity of managing states shared across components becomes daunting for code readability, testing, and future enhancements.

“First, solve the problem. Then, write the code.” — John Johnson

Let’s start with an example of an Authentication service (yes just Authentication service because in a world full of monolithic applications, we can be a service-driven application) in which we have a few prerequisite functions e.g. (Login, Signup, Forgot password, etc.). Let’s analyze various use-cases and the types of states we will encounter in our application

signup/login -

  1. user will enter user details and hit submit
  2. Call to /{signup or login} API, which will return a success or error.
  3. In case of success, the user will proceed to the next screen.
  4. In case of an error, we will show the user an error screen.

Now that we are clear with the requirements; let’s talk about state management libraries

Why not Redux?

React has many Third-party libraries such as Redux, Flux, and MobX. Here we will consider and compare Redux for the above example as it is famous out of all.

  • Redux is an infinite state machine (which means that it can have countless states).
  • Redux is essentially a state container where events (called actions in Redux) are sent to a reducer (plain functions that return the next state given the current state) that updates states.
  • Redux does not have a built-in way to handle side effects. There are many community options e.g.( redux-thunk, redux-saga, etc.)
  • Redux currently has no way to visualize transitions between states, since it does not discern between finite and infinite state.
  • Redux encourages the use of a single, “global” atomic store.

We still use Redux in most of our projects, and I’m quite happy with it. However Redux adds complexity even in a very straightforward project like we are considering in this example where we know we have a finite number of states.

Finite State Machine way —

A Finite State Machine is a mathematical model of computation. It’s an abstract concept whereby the machine can have different states, but at a given time fulfills only one of them. Here we will use the XState library for creating, interpreting, and executing finite state machines.

Now that we know why we choose XState and how our state will work in applications we can proceed with development, and the first thing to do is to define application logic. Here we will use the XState visualizer to plan it visually.

Visual representation of application logic in XState visualizer (https://xstate.js.org/viz/?gist=1e567cff699c55a1ce366eec721b5cd9)

As you can see in the above image, it’s an abstract representation of finite state machine of our Auth application. A finite state machine can be in only one of a finite number of states at any given time and events cause it to change state. Let’s see these states one by one:

  1. init — application’s initial state to determine if a user is logged in or not.
  2. auth — auth state will handle all the user authentication-related states that we will cover later in the blog and on successfully login or signup, the state will transit to profile state.
  3. profile — as the name suggests it will contain all the profile-related states ie. update profile, etc.
  4. logout — as the name suggests it will clear the user-related objects and the state will transit to auth state

XState library —

npm install xstate @xstate/react --save
# or:
yarn add xstate @xstate/react --save

And this is the basic XState code snippet for our Auth application. Don’t worry we will cover everything in detail 🐵

//machine.tsimport { createMachine } from "xstate";const appMachine = Machine({
id: 'app', // Name
initial: 'init', // Initial state
context: {
user:{}, // Store like redux store
},
states: {
// Business logic
}
});

State machines are defined using the createMachine() function which can be imported from XState library. It takes above machine config as a first argument and Implementations for actions, activities, delays, guards, and services can be referenced in the machine config as a string, and then specified as an object in the 2nd argument.

Machine config has the following properties:

  1. Id — unique identifier of the machine
  2. initial — initial state of the machine
  3. context — context is like redux data store(but less complicated 😆)
  4. states—list of possible states and their underlying logic.

Now let’s write(***understand) some code

Now that we know the basics of Xstate machine config. Let’s Create machine.ts file in the root folder and add the above code. now that we have basic setup ready we can start working on the application’s individual state which we will add inside states object:

A State definitions is an abstract representation of a system (such as an application) at a specific point in time. As an application is interacted with, events cause it to change state. A finite state machine can be in only one of a finite number of states at any given time. The current state of a machine is represented by a State instance

  1. init state —

this state will be used to check if the user is already logged in or not by determining the user’s authentication token in cookies. if we find an authentication token that is not expired then we can transit the user to the profile state as a logged user and if not then we will transit the user to auth state.

Event is what causes a state machine to transition from its current state to its next state. in case of init, depending on the auth token availability in the cookie we will fire an event which will help trasit machine from init to auth or profile state conditionally.

// inside states object in machine.ts   "init":{
"on":[
{
"cond":"isLoggedIn",
"target":"profile"
},
{
"cond":"!isLoggedIn",
"target":"auth"
}
]
}
Init state visualizer

As we can see in the above snippet, We have an attribute that will indicate the initial state of the application; and in our case, it will be Init state as we want to redirect the user to the appropriate page depending on his auth status(Yes we will write rendering logic based on user cookie detection but we will cover it later in the blog). here from Init it can only trasit to auth or Profile state depending on user’s auth token present in cookie.

2.auth state —

When the user’s auth token is not present in the cookie, machine will transit from initstate to authstate. this will render the login page by default and this page will have login form and buttons for signup and forgot password. The below image is visual representation of auth state. Inside auth state we have few child state which will handle the auth operations like login, signup and forgot password. by-default login state will be active as we have mentioned initial:"login" in the code.

https://xstate.js.org/viz/?gist=1e567cff699c55a1ce366eec721b5cd9
// inside states object in machine.ts below init object   "auth":{
"initial":"login",
"states":{
"login":{
"on":{
"ENTER_EMAIL":{
"actions":"cacheEmail"
},
"ENTER_PASSWORD":{
"actions":"cachePassword"
},
"SUBMIT":[
{
"cond":"isLoggedIn",
"target":"#app.profile"
}
],
"SIGNUP":"signup"
}
},
"signup":{
"on":{
"ENTER_FULL_NAME":{
"actions":"cacheFullName"
},
"ENTER_EMAIL":{
"actions":"cacheEmail"
},
"ENTER_PASSWORD":{
"actions":"cachePassword"
},
"REENTER_PASSWORD":{
"actions":"cacheReenterPassword"
},
"SUBMIT":[
{
"cond":"isLoggedIn",
"target":"#app.profile"
}
],
"LOGIN":"login"
}
}
}
}

Copy the above code inside below init object inside machine.ts. Now let’s understand auth’s child states one by one.

Login — As name suggests this will handle login related events (Don’t worry, we will cover how to handle events in Front-end integration 🤓).

ENTER_EMAIL event will handle user’s email input from login page form. here we can run various validation on the input if required

ENTER_PASSWORD event will handle user’s password input from login page form.

SIGNUP event will trasit machine from login state to signup state and signup page will render.

SUBMIT event will handle login form submission. this will make login API call to the server and upon response it will update isLoggedIn object state in context. in case of success machine will trasit from auth to profile state and in case of failure we can display error on form.

Signup — As name suggests this will handle Signup related events. Just like login state events, here ENTER_FULL_NAME will handle User name input, ENTER_EMAIL will handle email input and LOGIN event will trasit machine form signup to login state and will also render login page.

ENTER_PASSWORD ,REENTER_PASSWORD will handle password input and on top of that we will write logic to check if both the strings are same or not.

Just like Login state SUBMIT event, this SUBMIT event will handle signup logic.

🥳 Finally now we know basics of XState and how a basic authentication state machine will work. But hold on you must be thinking 🤔 where is the code to the front-end code!!!!!!!!!! Excuse me i wanna copy paste everything…☝️

WE DON’T DO THAT HERE IN UpGrad!!!!

Here in UpGrad we make sure while you are learning from UpGrad, you are understanding everything so that next time even though you’ll copy paste the code(let’s accept we all do that 🙈) but you’ll have all the understanding that you need to write XState code.

Although people say God Speed, I have divided this tutorial into TWO parts. In 2nd part I will cover React integration and various concepts like Interpreter, useService, action, invoke, etc. so stay tuned because we’ll be back in no time!!!

--

--