Recently I started thinking about state management for React applications. I’ve come to some interesting conclusions, and in this article I’ll show you that what we call state management isn’t really managing state.

Translator: The front end of the Ali Cloud – also tree

Md -state-in-javascript-with-state-machines- Stent

What is The elephant in The room

Let’s look at a simple example. Imagine a form component that displays a user name, password, and a button. The user will fill out the form and then click Submit. If all goes well, we have logged in and it is necessary to display a welcome message and some links:


var isLoggedIn;
isLoggedIn = false; // Display the form
isLoggedIn = true; // Display welcome messages and linksCopy the code

But that’s not enough. If the HTTP request we trigger after clicking the submit button takes some time to respond, we can’t leave the form alone on the screen and need more UI elements to show this intermediate state, so we have to introduce another state into the component.

Now we have a third presentation state that can’t be solved with just one isLoggedIn variable. Unfortunately we cannot set the variable value to false-ish, which is neither true nor false. Of course, we could introduce another variable like isInProgress. Once we send the request we will set the value of this variable to true. This variable tells us that the request is in progress and that the user should see the display status in load.

var isLoggedIn;
var isInProgress; 

// Display the form
isLoggedIn = false;
isInProgress = false;

// The request is in progress
isLoggedIn = false;
isInProgress = true;

// Display welcome messages and links
isLoggedIn = true;
isInProgress = false;Copy the code

Very good! We’re using two variables and we need to remember the values of the variables in each of the three cases. Looks like we figured it out. But the other problem is that we maintain too many states. What if we needed to display a successful request, or if everything went well we needed to tell the user “Yep, you logged in”, and two seconds later the message was hidden with a gorgeous animation and the final screen was displayed?



isLoggedIn
isInProgress
isInProgress
false
false
isSuccessful

var isLoggedIn, isInProgress, isSuccessful;

// Display the form
isLoggedIn = false;
isInProgress = false;
isSuccessful = false;

// The request is in progress
isLoggedIn = false;
isInProgress = true;
isSuccessful = false;

// Display success status
isLoggedIn = true;
isInProgress = false;
isSuccessful = true;

// Display welcome messages and links
isLoggedIn = true;
isInProgress = false;
isSuccessful = false;Copy the code

Our simple state management becomes a vast web of if-else conditions that are hard to understand and maintain.

if (isInProgress) {
  // The request is in progress
} else if (isLoggedIn) {
  if (isSuccessful) {
    // Display the request success message
  } else {
    // Display welcome messages and links}}else {
  // Wait for input, display the form
}Copy the code

We have another question that makes the situation worse: What do we do if the request fails? We need to display an error message and a retry link, and if retry is clicked we repeat the request.

Now our code is not maintainable at all. We have so many scenarios to satisfy that simply relying on introducing new variables is not acceptable. Let’s see if we can solve this with better naming, and perhaps introduce a new conditional declaration.

IsInProgress is only used during the request. We are also concerned now with the process after the request is completed.

IsLoggedIn is a little misleading because we set it to true as soon as the request ends. If the request goes wrong, the user is not actually logged in. So let’s rename it isRequestFinished. It looks better, but it just means we got the response from the server, and it doesn’t tell us if the response is an error.

IsSuccessful is a candidate variable with a suitable final state. We can set it to false if the request goes wrong, but wait, its default value is also false. So it can’t be used as a variable to represent an error state.

We need the fourth variable, how about isFailed?

var isRequestFinished, isInProgress, isSuccessful, isFailed;

if (isInProgress) {
  // The request is in progress
} else if (isRequestFinished) {
  if (isSuccessful) {
    // Display the request success message
  } else if (isFailed) {
    // Displays request failure information and retry links
  } else {
    // Display welcome messages and links}}else {
  // Wait for input, display the form
}Copy the code

These four variables describe a seemingly simple but actually not simple process that involves many boundary cases. As the project iterates further, more variables may end up being defined because the combination of existing variables does not meet the new requirements. This is why building user interfaces is so difficult.

We need better state management. Perhaps more modern and popular concepts could be used.

How about Flux or Redux?

Recently I’ve been thinking about the Flux architecture and the Redux library’s position in state management. Even though these tools are related to state management, they are not intrinsically designed to solve such problems.

Flux is the architecture that Facebook uses to build client-side Web applications. It complements the way React’s view components are organized with one-way data flows.

Redux is a predictable state container for building JavaScript applications.

They are “one-way data flows” and “state containers,” not “state management.” The concepts behind Flux and Redux are very practical and clever. I think they are the right way to build user interfaces. One-way data flow improves front-end development by making data predictable. The immutable feature possessed by Reducer in Redux provides a data transmission method that can reduce bugs. In my experience, these patterns are more suitable for data management and data flow management. They provide sophisticated apis for exchanging information that changes our application data, but do not solve our state management problems. This is also because these questions are highly project-specific and the context of the question depends on what we are doing. Of course we can handle things like HTTP requests through a library, but we still need to write our own code for other business logic. The question is how do we organize the code in a way that doesn’t mean we have to rewrite the entire application every two years.

A few months ago I started looking for patterns that could solve the problem of state management, and I finally discovered the concept of a state machine. We actually build state machines all the time, we just didn’t know it.

What is a state machine?

The mathematical definition of a state machine is a computational model, as I understand it: a state machine is a box that holds your states and state changes. There are a few different kinds of state machines, and the one that applies to our case is a finite state machine. As its name suggests, a finite state machine contains a finite number of states. It takes an input and based on that input and the current state determines the next state, which can be output in many cases. When a state machine changes states, it is said to transition to a new state.

Combat state machine

In order to use a state machine we more or less need to define two things – states and possible transition methods. Let’s try to implement the form requirements mentioned above.

In this table we can clearly see all the states and their possible outputs. We also define the next state if the input is passed into the state machine. Writing a table like this will help your development cycle because it will answer the following questions:

  • What are all the possible states for a user interface?
  • What happens between each of these states?
  • If a certain state changes, what is the result?

These three questions can solve a lot of problems. Imagine having an animation effect when we change the content presentation, and when the animation starts, the UI is still in the same state and the user can still interact. For example, the user clicks the submit button twice very quickly. If state machines are not applicable, we need to prevent code execution by marking variables with if statements. However, if we go back to the table above, we can see that the Loading state does not accept input from the Submit state. So if we put the state machine into loading state after the first button click, we will be in a safe position. Even if the Submit input/action is sent, the state machine ignores it and of course does not issue another request to the back end.

The state machine model works for me. There are three reasons to use a state machine in my application:

  • The state machine mode saves a lot of potential bugs and weird cleaning because it doesn’t let the UI change into a state we don’t know about.
  • The state machine does not accept undefined input as the current state. This frees us from some of the fault-tolerant processing we perform on other code.
  • State machines force developers to think declaratively. Because most of our logic needs to be defined in advance.

Implement state machines in JavaScript

Now that we know what a state machine is, let’s implement one and solve the problem we started with. Define a simple object literal with some nested properties.

const machine = {
  currentState: 'login form'.states: {
    'login form': {
      submit: 'loading'
    },
    'loading': {
      success: 'profile'.failure: 'error'
    },
    'profile': {
      viewProfile: 'profile'.logout: 'login form'
    },
    'error': {
      tryAgain: 'loading'}}}Copy the code

This state machine object defines the state using the contents of our table above. As in the example, when we are in the login form state, we use Submit as an input and should end in the loading state. Now we need a function that accepts input.

const input = function (name) {
  const state = machine.currentState;

  if (machine.states[state][name]) {
    machine.currentState = machine.states[state][name];
  }
  console.log(`${ state } + ${ name } --> ${ machine.currentState }`);
}Copy the code

We get the current state and check that the input provided is valid, and if it passes, we change the current state, or in other words, transition the state machine to a new state. We provide a log output for input, current state, and new state (if any). Here’s how to use our state machine:

input('tryAgain');
// login form + tryAgain --> login form

input('submit');
// login form + submit --> loading

input('submit');
// loading + submit --> loading

input('failure');
// loading + failure --> error

input('submit');
// error + submit --> error

input('tryAgain');
// error + tryAgain --> loading

input('success');
// loading + success --> profile

input('viewProfile');
// profile + viewProfile --> profile

input('logout');
// profile + logout --> login formCopy the code

Note that we tried to break the state machine by sending the tryAgain state with the login form state or by repeating the commit request. In these scenarios, the current state is not changed and the state machine ignores the input.

The last word

I don’t know if the state machine concept works for your own scenario, but it works for me. I just changed the way I handled state management. I suggest giving it a try. It’s definitely worth it.

Ps: as always, an advertisement: Ali Cloud for front-end engineer, base Beijing or Hangzhou, please contact: [email protected]