Photo by NESA by Makers on Unsplash

When we started with the concept and architecture for a new project about a year ago, we questioned our “go to” state management for UI apps — Redux. We had just finished a social intranet with React and React Native, where we extracted all business logic and all code related to state management into a separate package in order to share it between the two apps. Redux had become our “go to” library because it fit most use cases reasonably well — we could write isolated frontend widgets to be included in CMS-rendered websites, as well as complete single-page applications with client-side routing. In either case, it allowed us to clearly separate business logic from appearance which is something that is really important to us.

Our new project was a greenfield, multi-tenant mobile point of sale (POS) software for the fashion industry— and therefore a bit different to what we were used to. Thinking about POS software, it became clear to us that we probably shouldn’t throw our usual solution at this problem. In other projects we used react-router to declaratively define our routes and render the app according to the current URL. Bringing this into the sample use case of buying a classic Nike Air Force 1 we realised that accessing the URL after the transaction is done should result in a different view, since the transaction is in a different state.

Wait. State — as in…

State Machine?

As I mentioned, we had just finished a big project with Redux, and our team was hesitant to use it again without properly reflecting its usage and looking for alternatives. We had realised that using Redux on a large-scale application could result in bloated code and huge complexity. Most of our bugs were related to the reducers, as they had to reduce a lot of data and were called on many different occasions. Looking for an alternative while thinking about “stateful” transactions, we started to invest time in state machines and implementations thereof. As shown in our last post, we took a liking to XState and decided to go with that.

So how do we connect the state machine to our React app? There are basically three steps to be done:

Defining the State Machine Exposing the State Machine to the React App Declarative State Matching

There’s a lovely recipe by David Khourshid in the XState docs about these steps as well, so if you’re starting with XState make sure to look into that as well.

Defining the State Machine

Configuring your state machine, figuring out what states you’ll need and what transitions to guard is not a trivial task. There is a lot of whiteboard sketching involved and yet, we re-did this several times and had a lot to learn. For now we’ll stick with the technical aspects of defining the state machine.

import { Machine } from 'xstate'; export default Machine({

"initial": "In Stock",

"states": {

"In Stock": {

on: { "Lend": "Lent" },

},

"Lent": {

on: {

"Return": "In Stock",

"Damage": "Damaged",

"Lose": "Lost",

},

},

"Damaged": {

on: { "Repair": "In Stock" },

},

"Lost": {

on: { "Find": "In Stock" },

},

}

});

You might remember this state machine from the previous article.

If you want to see how the machine behaves, you can copy and paste the machine configuration to the XState Visualizer and play around with it.

In theory we have a running state machine now, but we need some way to actually interpret transitions, keep track of state changes and expose it to our app.

Exposing the State Machine to the React App

When we started with XState and React, hooks were not yet released, but merely visible on the far horizon. So we built our own context provider that exposed the current state, and a state-transition function through a context consumer.

Luckily, these days there is the @xstate/react package, which provides a useMachine hook that can be used like this:

import React from 'react';

import { useMachine } from '@xstate/react'; import bookMachine from './state-machine'; export function Book() {

const [current, send] = useMachine(bookMachine); if (current.matches('In Stock')) {

return (

<div>

<p>Currently in stock!</p>

<button onClick={() => send('Lend')}>Lend</button>

</div>

);

} /* ... */

};

Let’s quickly go through what’s going on here. The bookMachine is passed to the useMachine hook. If the current state matches In Stock , as defined in the state diagram, “Currently in stock!” is rendered. Clicking the “Lend”-button results in a state transition to the Lent state. With this hook, we’ve basically connected our app to our state machine in order to render conditionally and transition between different states.

Declarative State Matching

While consuming the current state machine and using the state-transition function, send works like a charm with the provided package, our “routing”-logic got a bit messy. We wanted declarative state matching and took inspiration from the react-router package we had used in other projects. Our goal is an API similar to this:

function App() {

return (

<Match state="In Stock">

<p>In Stock</p>

</Match> <Match state={[

"Damaged",

"Lost",

]}>

<p>Forever gone</p>

</Match>

);

}

To achieve this, we need a component which renders conditionally if the current state matches a given state identifier. Once again we can take advantage of the useMachine hook to get the current state and compare it to the expected state.

export function Match({ children, state }) {

const [current] = useMachine(bookMachine); const expected = Array.isArray(state) ? state : [state];



return expected.some(value => current.matches(value))

? <>{children}</>

: null;

};

With this, we can now declaratively decide what components should be rendered based on the current state of our state machine.

Wrap up

XState is not only a neat JavaScript implementation of state machines, but also helps to integrate the state machine in your app as smoothly as possible. There are recipes in the documentation on how to use state machines with React and how to use them with Vue.

After working on the POS app for over six months, we are really happy with the React + XState combination. The code base is way cleaner than it used to be with Redux (although that’s probably due to how we used it, and not Redux’ fault). Even though the app has grown quite a bit it’s easy for freshly onboarded team members to understand the its features (especially with the XState Visualizer) and also become aware of edge cases in the statechart.