Why XState is THE State Management Tool
Written by: Matias Hernandez
Why XState is THE state management tool that you're missing out on.
Developing user interfaces is hard work, and we know it. Today, developing a UI is all about building representation of a specific state and communicating the state changes using components. You could argue that user interfaces are a type of graph, or in other words, they can be connected to other things in a non-hierarchical fashion. This opens Pandora's box, creating several possible states with different use cases and numerous edge cases. So, the main work that web developers do is manage the state change. These changes trigger the rendering process updating the UI accordingly. And there are a lot of tools to perform this task.
What is XState?
According to XState's documentation
- XState is a library for creating, interpreting, and executing finite state machines and statecharts, as well as managing invocations of those machines as actors.
Sounds pretty complex, right? Well, it should not be that way. Let's break that down!
In a nutshell, XState is a library that builds on top of essential concepts of Computer Science. XState helps you define - in a declarative way - all of your application's possible states or business logic and then interact with it with ease and confidence.
Those foundational Computer Science concepts are Finite State Machines and State Charts. At its core, a finite state machine is a mathematical model that describes the behavior of a system that can only be in one state at any given time. It sounds similar to how a UI can only represent a single state at a time.
A State Machine defines a finite number of states, a limited number of events, and a set of transitions between states that are triggered by the defined events. This provides predictability about what state is the next one. If you're familiar with React, you can think of this like useReducer on steroids. On the other hand, we have State Charts, which are formalisms to model reactive systems. They are an extension of state machines that enable new features like:
- Guarded transitions (or conditional transitions)
- Actions (functions that trigger on particular events like on entry or exit a state)
- Hierarchical states
- History, etc
This is the state chart of a state machine that performs a fetching action. How would you create that state machine in XState? Here's an example:
If you are a React developer, compare this declarative approach in XState with how it could look using the useReducer pattern:
So, what are you missing out on by not using XState?
Web application development can be expressed as rendering UI and managing state. Many tools exist to simplify these processes; however, some complexities are difficult to avoid, such as ever-changing UI requirements, managing state transitions, and component communication. XState provides a way to implement a state orchestration pattern. State Orchestration is a way to describe an event-driven architecture that relies on events and side effects to transition between states.
- David Kourshid, author of XState, recently tweeted about this idea of "state orchestration":
- Been thinking about this a lot. The common principle of "separation of concerns" is often blindly applied, leading to fragile architecture. Orchestration is the missing part.
The use of XState and the state orchestration pattern helps to avoid commonly encountered bugs when working with other "Non finite-state" solutions. These are widely called impossible states, which let's dive into now.
What are impossible states, and how XState avoids them
States machines and statecharts are excellent communication tools about the logic you want to build since non-developer teammates generally understand them. Diagrams help a lot in the territory. A state is a set of variables and values representing the (in this case) UI in any given point. Every new variable or value that you add causes the possible combination of states to increase. For example, you'll have 16 possible states with four independent boolean variables. As the name implies, an impossible state is a state that should not happen in the UI. Let's see an example based on the fetch process previously shown. Imagine a button that triggers a fetch call and three state variables to handle state changes: users, isLoading, and error. Here you can see the handler function for this use case:
Can you spot the bug? What happens if the user clicks the fetch button (triggering this function), the fetch fails, and the user clicks the button again? The final state will be an error and have newData. This shouldn't be possible! This impossible state happens because the function does not clear the previous error. We could fix this by adding a check when the success state happens to set the error as null, which increases code complexity.
Another option is to use the reducer pattern within a React component, as in the previous example. There is still another bug to consider. What happens if the user is too anxious and clicks the button many times? The application may move quickly between states and end up undesirable. How to fix this one? Yeah! Let's throw another boolean flag to disable the button and not trigger the fetch!. Now the simple code looks like this:
This code works, but the logic is becoming complex to read, and we should always follow the maxim: write code for other humans! Now, How can XState help you in this case? In this example, you need a way to manage the state that effortlessly defines the possible states and how these states relate to each other. Only one state should occur at a time, and it should be possible to identify the next state. In other words, we want a state machine. Let's see how an XState state machine will look for this same case.
This code can be daunting at first sight, so let's break it down.
How an XState machine is built
- id: An identifier as a string is optional but helpful when you want to debug or even connect multiple machines
- initial: Identify the initial state of the machine. It is a simple string that declares the state before any action or transition is made.
- context: Is an arbitrary object that works as an "internal machine state ."It can store any data required by the application. If you're wondering the difference, the basic gist is: Anything that cannot be expressed finitely should be held as context inside the machine. Check this article from Matt Pocock about it
- states: This is the meat of the machine. This is where all the states are declared, including the actions and transitions that can happen in each state. Each key of this object matches a particular state.
- on: This is a state property that helps you model the actions and possible transitions that the current state can react to.
The first impression of this machine is expected that this looks like more work or that it is too cluttered, and is true, a machine definition can quickly go wild in terms of size but let's check the good side of this:
- Is simple to see all possible states of the machine
- You don't have to track any variable besides the ones you define in the context (if it is required)
- You can fully decouple the state management logic (the machine) from the UI that will react to the machine.
- There are no impossible states.
- You can always get the machine diagram or draw the diagram and then declare the machine.
Copy and paste the above code in the visualizer, and you'll see the following image:
Use the state machine with React, Vue, and Svelte
To check how to use xstate with a particular framework or library, let's review a "real world" example. Let's build a video player managed by xstate, and let's share the machine declaration among different UI implementations. Let's first define what the requirement is: You were requested to build a video player UI, the player should: Load the video from an external source have buttons to play, pause, reset, and stop the video Looks simple right? Let's write the state machine!
- If you want to jump into the complete code directly, check this code sandboxes
- React version
- Svelte version
- Vue version
Let's think in what states the player will possibly be:
- Loading or waiting
- Ready to play
The last three states are only possible if the player is ready to play. Let's set that into a machine declaration.
One crucial thing to notice here is that a machine can have nested machines inside of this. This is called Hierarchical states. This helps a lot to declare states tied to another state like in this case. The playing state can only be if the ready state is on.
Now is time to add the logic of how the machine will move from one state to another: Transitions.
The machine looks way more extensive, but we have all the possible states and each "event" that a state can take. The next step is to define the transitions, meaning, to what state the machine should go when a particular event happens.
And that can be represented by the following chart.
Here you can see the transition from one state to another. So the final step is to declare some actions that need to happen when a transition is fired. These actions will be defined externally based on each implementation of the UI, so you reference them (with a named string) inside each transition.
Time to React
Let's bring React into the mix as an example of using the machine.
XState offers a package for each significant framework. In this case, you need to use @xstate/react This will provide some hooks that will work with the UI. In this case, you'll use useMachine hook. This hook accepts a machine as an argument and also some configuration object that you'll use to define the actions. The UI will be a simple video tag and some buttons, so to know the state of the content, you'll need to use a ref. The ref is one that you'll use inside the declared actions. The useMachine hook will return a tuple (same as the useState hook) with the state of the machine and a function that will trigger transitions (send).
Next, we need a way to know the machine's state so the UI can react to it, like disabling a button. For that, you'll use the matches method from the state. This method determines if the current state is a subset of the state passed as an argument.
- There is another hook that allows you to use selectors: useSelector. This hook has better performance since it will only cause a re-render if the selected value changes. Check the documentation here
So this lets you build the following "flags."
Now is the time to add the JSX for the UI. The UI will use the above flags and the send method returned by the useMachine hook to trigger transitions.
And there you have it, a full implementation of a video player based on a machine fully decoupled from the UI itself.
The main job of a web developer is all about managing the state of a dynamic web application. This ever-changing state creates complexity since it can lead to different combinations of states or even impossible states. Many tools or libraries aim to help you with this task in very different ways. Still, just one uses solid computer science for its design and usage. XState handles user events and the resulting UI changes in a simple way. In this article, you were able to check out:
- What is XState?
- What is a Finite State Machine and State Charts.
- What are impossible states?
- How XState helps you manage the state changes.
If you enjoyed reading this you might enjoy these:
Why We Choose Typsescript All the Way Through
Want to peak into our daily work? Our coaches recount real world situations shared as learning opportunities to build soft skills. We share frameworks, podcasts and thinking tools for sr software developers.
Keep on readingGo to Blog home
The (remote) opportunities
We expect professionalism and client service, so we can offer a deeply caring experience for our clients. In return, you get freedom to work wherever you want. No timesheets, no big brother watching every move. We trust you to know what’s best to find the right solution.