XState is a very well written library. It seeks to bring the finite state machine pattern into React applications and, for the most part, does exactly that, and extremely well.
I’ve been using — well, forced to use, really — XState in a professional application in my job. I didn’t agree with the decision at the time, but didn’t have enough hands-on experience to articulate why it was a bad decision, just a gut feeling. Now, having worked with it, I have an idea of the fundimental flaw in it.
XState should never, ever, ever, be used in an actual React application that will go into production.
Again, it’s not that XState is bad. It’s just wrong for React.
Wrong way on the Roundabout
To understand the basic problem, you have to do a bit of a dive into what a “finite state machine” is. To oversimplify, it’s a software design pattern that allows an object to alter it’s behavior when the internal state changes.
Imagine a fan with four “states”: Off, High, Medium, Low. When the state is “Off”, the fan does not spin. When the state is Low, the fan spins, but it spins at different speeds for Medium and High. Switching the state (that is, pushing the button/turning the knob) results in changing the behavior.
Now, this pattern comes from Object-Oriented design, specifically, Design Patterns: Elements of Reusable Object-Oriented Software, a work which is often quoted along with K&R’s The C Programming Language as a “must read” for software design. It’s not a bad pattern.
While React is not purely functional in the same way that, say, Elm is, React’s team has always strived to make React as functional as possible.
As part of this “functional-preferred” philosophy, React’s developers came up with something called the “Flux Pattern” — a unidirectional data flow where an action runs through a dispatcher, the dispatcher updates the store, and the store updates the view. Or in other words:
Flux: Dispatching an action changes the state, which updates the view, which may queue up a new action.
The “state pattern”, as defined by the Gang of Four, also address the same problem of managing state. It also has unidirectional data flow.
It’s just that the direction of the unidirectional data flow in finite state machines is the opposite direction from the flux pattern.
XState/FSM: Changing the state dispatches an action, which may change the context, which may update the view, which may or may not queue up a new state change.
If Flux is your standard British clockwise roundabout, XState sends cars careening around it in a counterclockwise direction like a drunk American tourist.
The end result of all this is that rather than working with React’s “natural” data flow, XState actively works against it, swimming upstream. It’s not that it can’t be used in React — indeed, I have used it, which is why I’m writing this article. It’s that it makes following the flow of data difficult, creates horribly complex and tightly coupled software, and ultimately makes a mess of things.
Eat the cake, then bake it.
One of the reasons I think the Flux pattern is so popular — not just in React but in similar libraries such as Vue — is because it makes asynchronous functions that dispatch actions relatively simple. If you put all the asynchronous logic in the function that calls the dispatcher, then you can defer the dispatching of the data to the store until that action resolves. By the time the dispatcher is called, you not only know what you want to do, but also it’s result.
This may not seem like much, but the advantage of this is that you can compartmentalize actions. It allows for “loosely coupled, highly cohesive” code by seperating the dispatch function from the data. That is — dispatch is independent of the state of the store. It encourages simplicity, testability, and code reuse.
But in XState, by forcing a state change as the first step in the process, you lock yourself into only dispatching when the store is in a certain state. It is tightly coupling state to dispatch. (This is actually one of the core reasons for the original design of the state pattern: to prevent the execution of a function if the machine is in the wrong state.)
Because of this tight coupling, dispatches can’t really be reused. They have to be defined, independently, for each state and for each event that state can handle. This can get very complex, very quickly.
This is especially true when you have actions that execute multiple asynchronous function calls. If this happens, you can use standard JS Promises or Async/Await to correctly defer the logic of the state calls, dispatching in the correct order for the correct reasons. Again — it’s not impossible to do this with XState, but it does mean building out branching pathways for each possible action, with each dispatch triggering another state change.
And the biggest problem: while it’s not 100% functional, the “functional-preferred” approach to Flux reduces the number of side effects to a minimum, whereas XState is not just reliant on side effects, it is designed around the mandatory use of side effects.
Designed Around Side Effects
Generally speaking, you want to avoid adding more side effects than you need to in programming. There’s a couple reasons why — the lack of side-effects makes it easier to verify and test a program, you can always be sure that a function given the same parameters can return the same answer (known as referential transparency or “pure functions”). It’s just easier to deal with. That’s not to say that you can always avoid side effects, but it’s best to minimize them.
Even without using the Ministry of Functional Programming Propaganda terms such as “referential transparency”, it just helps that pure functions are simple to understand, use, reuse, and extend.
XState, on the other hand, is designed around side-effects. It’s actually touted as an advantage by XState’s creator. (The fact that Redux doesn’t handle — or need — side effects is a big reason that particular state manager surged in popularity to become the de-facto React state manager solution.)
And there’s really no way to reduce side effects — the whole point of XState is that the change in state triggers an action as a side effect of that state change.
Talking again about composing multiple asynchronous actions in XState, the only way to compose those multiple actions would be to trigger state changes as side effects to actions, which themselves are side effects to state changes, and you often find yourself hopping from state, to action, to state, to action, trying to follow a maze of directions in order to follow the logic of the code. Instead of a linear path, you essentially follow a graph of nodes; easy for a computer to do, but extremely difficult for a human programmer tracking down a bug to do.
A Series Of Unfortunate Event Handlers
Because actions in FSM/XState are side-effects, the result is that instead of responding to changes in the state, we end up more often than not having to listen for those changes instead. This can cause serious problems especially when dealing with changes at the React component level, if changes in state need to result in changes in the view. Often, in order to work with React’s native “setState/useState” capabilities, we have to include a useEffect() to listen for changes to state, at which point a side-effect has to trigger the setter for the state. It’s a kludge, and a nasty one at that. Remember, useEffect() is how React handles side-effects — the less side effects there are to handle, the less complex the application.
A very good idea, well implemented, for the wrong problem.
And all of this is not to say that XState isn’t a fine solution for finite state machines in science and system modeling. If you’re a maths buff, and are looking for a way to model or simulate existing finite state machines in JS, this is a good library to do so. But I wouldn’t pair it with React.
There is a huge difference between what is mathematically elegant and what makes good working software. (Imaginary numbers like √-1 are a great mathematical concept too, but I wouldn’t use them on my tax return. Similarly, I wouldn’t want to use a mobius strip to keep my pants up, or a Klein Bottle as a tea kettle.) Finite State Machines are a great way to describe systems as a model, but not a great way to build them into existence.
And that’s the core flawed assumption at the heart of XState: As counter-intuitive as it may be to say, React application state management isn’t about managing state, it’s about managing data inside a state, and reacting to changes in the data.
It’s a good solution for a problem React app developers don’t have.