You are managing state? Think twice.
Recently I started questioning the state management in React applications. I’ve made some really interesting conclusions and in this article I’ll show you that what we call a state management may not be exactly about managing state.
The elephant in the room
Let’s take a simple profile component. Imagine that this is a component that displays a username field, password field and a button. The user will add his/her credentials inside and will submit the form. If everything is ok and we are logged in we have to display a welcome message and a couple of links:
Looking at this mockup we may assume that we have two states of our component. A state where the user is not logged in and a state where the user is logged in. So we start by managing these two states. Let’s say that we have a flag, a boolean that tells us the status of the user.
var isLoggedIn;
isLoggedIn = false; // display the form
isLoggedIn = true; // display welcome message + links
That is good but it is not enough. What if the HTTP request that we fire after clicking the submit button takes more time. We can’t leave the form on the screen. We need some more UI that represents this moment. We have to add another state to our component.
Now we have a third state and the logic that uses isLoggedIn
variable can’t handle it. Unfortunately we can’t set false-ish
as a value. It is either true
or false
. Sure, we will bring another variable like isInProgress
for example. Once we fire the request we will change it to true
. That will indicate that we are in a process of saving and the user should see the spinner rendered.
var isLoggedIn;
var isInProgress;
// display the form
isLoggedIn = false;
isInProgress = false;
// request in flight
isLoggedIn = false;
isInProgress = true;
// display welcome message + links
isLoggedIn = true;
isInProgress = false;
Nice! Two variables, we just have to remember these three cases above and what values we have set in. It looks like we are solving our problem right. Not really. The thing is that we may have more states. What if I tell you that we need to display a successful request message. If everything goes ok we need to inform the user saying “Yep, you logged in successfully”. Two seconds later we hide the message with a fancy animation and display the final screen.
Now it gets a little bit more complicated. We have isLoggedIn
and isInProgress
but it looks like we can’t use only them. isInProgress
is false
indeed when the request finishes but it is false
also by default. I guess we need a third variable - isSuccessful
.
var isLoggedIn, isInProgress, isSuccessful;
// display the form
isLoggedIn = false;
isInProgress = false;
isSuccessful = false;
// request in flight
isLoggedIn = false;
isInProgress = true;
isSuccessful = false;
// display a successful message
isLoggedIn = true;
isInProgress = false;
isSuccessful = true;
// display welcome message + links
isLoggedIn = true;
isInProgress = false;
isSuccessful = false;
Step by step our simple state management transforms into a giant net of if-else conditions which are difficult to follow and maintain.
if (isInProgress) {
// request in flight, render a spinner
} else if (isLoggedIn) {
if (isSuccessful) {
// request successful, render a message
} else {
// render welcome message + links
}
} else {
// waiting for input, render login form
}
To make the situation worst we will ask a final question - “What happens when the request fails?”. Let’s say that we have to display an error message with a link Try again
and if clicked we have to repeat the request.
Now we are in a situation where our code is not exactly scalable. We already have couple of cases to satisfy and bringing a new variable is not acceptable. Let’s think and see if it is a matter of better naming and maybe one more conditional statement.
isInProgress
is used only when the request is in flight. We are now interested in the processes after that.isLoggedIn
is a little bit misleading because we have it set totrue
when the request finishes but what if we have an error. The user is not exactly logged in right. So we may rename it toisRequestFinished
. Looks better but it only indicates if we get a response from the server. We can’t use it for the error state.isSuccessful
is a good candidate for that final state. We may set it tofalse
if there is an error. But wait, it isfalse
by default. So no,isSuccessful
can not be used as a representation of the error state.
We need a fourth variable. What you think about isFailed
?
var isRequestFinished, isInProgress, isSuccessful, isFailed;
if (isInProgress) {
// request in flight, render a spinner
} else if (isRequestFinished) {
if (isSuccessful) {
// request successful, render a message
} else if (isFailed) {
// render error message + try again link
} else {
// render welcome message + links
}
} else {
// waiting for input, render login form
}
These four variables are the elephant in the room. It is there but we don’t see it or just ignore it. These variables describe a process. Process that looks simple but in fact is not and has couple of edge cases. When the project moves forward we will have to cover new requirements and we will probably end up having more flags just because the combination of the defined ones doesn’t satisfy our goals. That is why building UI is difficult. We have lots of state and this state needs to be controlled properly.
We’ll say a better state management is required. But how? Maybe some of the modern and trendy concepts?
What about Flux or Redux?
Recently I’m questioning the role of Flux architecture and Redux library in the context of the state management. Even though this tools are associated with state management it looks like they are not exactly about that.
Flux is the application architecture that Facebook uses for building client-side web applications. It complements React’s composable view components by utilizing a unidirectional data flow.
Redux is a predictable state container for JavaScript apps.
“unidirectional data flow” and “state container” but not “state management”. The concepts behind Flux and Redux are really useful and pretty smart. My opinion is that this is the proper way for building UI. One-direction data flow that leads to predictability makes the front-end development a lot better. The immutability nature of the Redux’s reducers provides a bug-free data transfer. However, I kind of feel that such patterns are more about data management or data flow management. They provide nice API for exchanging messages that modify the data of our applications but they can’t solve our state management problems. And that’s because these problems are project specific. They have context which depends on what we are doing. Of course some tasks like making a HTTP request may be handled by a library but for the other business-logic related operations we need custom code. The question is how to write that code in a proper way so we don’t rewrite the whole application twice a year. Because that is what happens if we follow approach like the one above.
A couple of months ago I started looking for patterns that solve such state management issues. And I found the concept of state machines. I am cursed now because I see state machines everywhere. In fact we are all building state machines for years. We just don’t know it.
What is a state machine?
The mathematical explanation is that the state machine is a model of computation. The non-mathematical (my explanation) is that the state machine is a box that keeps your state and that state changes over time. There are different types of state machines but the one that fits in our case is finite state machine. As the name suggests it has a finite number of states. This type of machine accepts an input and based on that input plus the current state decides what will be the next state. It may also produces some output. When the machine changes its state we say that it transitions to a new state.
Working with a state machine
To work with state machine we more or less have to define two things - states and possible transitions. Let’s do it for the example above. The one with the log in form.
In this table we clearly see all of our states and their possible inputs. We also defined what is the next state if some of the inputs is send to the machine. Having to write such table brings lots of value to your development cycle because it asks the right questions:
- What are all the possible states that this UI may be in?
- What exactly could happen in every of the states?
- If a certain thing happens what is the result?
These three questions have the potential to solve lots of problems. Imagine how we have an animation when changing the screens. While the animation is happening the old UI is still there for some time and unfortunately the user is able to interact with it. For example there is a chance that the user submits the log in form twice by just clicking on the submit button again really quick. Without a state machine we will have to protect code execution with if
statements by using flags or something similar. However, if we take the table above we may see that the “loading” state does not accept a “Submit” input. So if we transition the machine to “loading” state after the very first click of the button we are in a safe position. Even though the “Submit” input/action is dispatched the state machine will ignore it and it will not fire a request to the back-end.
The state machine pattern happened to fit in many places to me. I identified three benefits that make me think about using state machine all over my applications:
- The state machine pattern eliminates bugs and weird situations because it wont let the UI transition to state which we don’t know about.
- The machine is not accepting input which is not explicitly defined as acceptable for the current state. This eliminates the need of code that protects other code from execution.
- It forces the developer to think in a declarative way. This is because we have to define most of the logic upfront.
Implementing a state machine in JavaScript
Now, when we know what a state machine is let’s implement something. Let’s solve the problem which we started with. Simple object literal with couple of 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'
}
}
}
The machine
object defines the states of the machine following the rules that we placed in the table above. Like for example when we are at the login form
state and we send submit
as an input we should end up in a loading
state. Now we need a function for accepting inputs.
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 }`);
}
We get the current state and check if the provided input is acceptable machine.states[state][name]
. If yes then we change the current state or in other words transition the machine to a new state. In both cases we provide a log of the operation showing the current state, what is the input and and what is the new state (if changed at all). Here is how to exercise 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 form
Notice how we try to break it by sending tryAgain
when the machine is in a login form
state or sending submit
twice. In those cases the current state is not changed and basically the machine ignores the input.
Final words
I don’t know if the state machine concept makes sense in your context but for me it definitely does. I simply changed the way of how I handle state management. I’ll suggest to give it a go. It definitely worth it.
I like Flux architecture and I’m using Redux together with redux-saga every day. These stuff are awesome but they do not solve state management problems easily. After implementing a state machine couple of time I decided to combine what I learned with my love to Redux and write a library. Stent is a Redux-liked tool that works well with React (and not only). It provides data and state management in one place. I will be more then happy to see someone trying it and posting some feedback either here or in the GitHub repository of the project.
If you enjoy this article make sure that you check also the following one "Getting from Redux to a state machine". It's about converting an app from Redux to Stent.