Robust React User Interfaces with Finite State Machines

Avatar of David Khourshid
David Khourshid on (Updated on )

User interfaces can be expressed by two things:

  1. The state of the UI
  2. Actions that can change that state

From credit card payment devices and gas pump screens to the software that your company creates, user interfaces react to the actions of the user and other sources and change their state accordingly. This concept isn’t just limited to technology, it’s a fundamental part of how everything works:

For every action, there is an equal and opposite reaction.

– Isaac Newton

This is a concept we can apply to developing better user interfaces, but before we go there, I want you to try something. Consider a photo gallery interface with this user interaction flow:

  1. Show a search input and a search button that allows the user to search for photos
  2. When the search button is clicked, fetch photos with the search term from Flickr
  3. Display the search results in a grid of small sized photos
  4. When a photo is clicked/tapped, show the full size photo
  5. When a full-sized photo is clicked/tapped again, go back to the gallery view

Now think about how you would develop it. Maybe even try programming it in React. I’ll wait; I’m just an article. I’m not going anywhere.

Finished? Awesome! That wasn’t too difficult, right? Now think about the following scenarios that you might have forgotten:

  • What if the user clicks the search button repeatedly?
  • What if the user wants to cancel the search while it’s in-flight?
  • Is the search button disabled while searching?
  • What if the user mischievously enables the disabled button?
  • Is there any indication that the results are loading?
  • What happens if there’s an error? Can the user retry the search?
  • What if the user searches and then clicks a photo? What should happen?

These are just some of the potential problems that can arise during planning, development, or testing. Few things are worse in software development than thinking that you’ve covered every possible use case, and then discovering (or receiving) new edge cases that will further complicate your code once you account for them. It’s especially difficult to jump into a pre-existing project where all of these use cases are undocumented, but instead hidden in spaghetti code and left for you to decipher.

Stating the obvious

What if we could determine all possible UI states that can result from all possible actions performed on each state? And what if we can visualize these states, actions, and transitions between states? Designers intuitively do this, in what are called “user flows” (or “UX Flows”), to depict what the next state of the UI should be depending on the user interaction.

Picture credit: Simplified Checkout Process by Michael Pons

In computer science terms, there is a computational model called finite automata, or “finite state machines” (FSM), that can express the same type of information. That is, they describe which state comes next when an action is performed on the current state. Just like user flows, these finite state machines can be visualized in a clear and unambiguous way. For example, here is the state transition diagram describing the FSM of a traffic light:

What is a finite state machine?

A state machine is a useful way of modeling behavior in an application: for every action, there is a reaction in the form of a state change. There’s 5 parts to a classical finite state machine:

  1. A set of states (e.g., idle, loading, success, error, etc.)
  2. A set of actions (e.g., SEARCH, CANCEL, SELECT_PHOTO, etc.)
  3. An initial state (e.g., idle)
  4. A transition function (e.g., transition('idle', 'SEARCH') == 'loading')
  5. Final states (which don’t apply to this article.)

Deterministic finite state machines (which is what we’ll be dealing with) have some constraints, as well:

  • There are a finite number of possible states
  • There are a finite number of possible actions (these are the “finite” parts)
  • The application can only be in one of these states at a time
  • Given a currentState and an action, the transition function must always return the same nextState (this is the “deterministic” part)

Representing finite state machines

A finite state machine can be represented as a mapping from a state to its “transitions”, where each transition is an action and the nextState that follows that action. This mapping is just a plain JavaScript object.

Let’s consider an American traffic light example, one of the simplest FSM examples. Assume we start on green, then transition to yellow after some TIMER, and then RED after another TIMER, and then back to green after another TIMER:

const machine = {
  green: { TIMER: 'yellow' },
  yellow: { TIMER: 'red' },
  red: { TIMER: 'green' }
};
const initialState = 'green';

A transition function answers the question:

Given the current state and an action, what will the next state be?

With our setup, transitioning to the next state based on an action (in this case, TIMER) is just a look-up of the currentState and action in the machine object, since:

  • machine[currentState] gives us the next action mapping, e.g.: machine['green'] == {TIMER: 'yellow'}
  • machine[currentState][action] gives us the next state from the action, e.g.: machine['green']['TIMER'] == 'yellow':
// ...
function transition(currentState, action) {
  return machine[currentState][action];
}

transition('green', 'TIMER');
// => 'yellow'

Instead of using if/else or switch statements to determine the next state, e.g., if (currentState === 'green') return 'yellow';, we moved all of that logic into a plain JavaScript object that can be serialized into JSON. That’s a strategy that will pay off greatly in terms of testing, visualization, reuse, analysis, flexibility, and configurability.

See the Pen Simple finite state machine example by David Khourshid (@davidkpiano) on CodePen.

Finite State Machines in React

Taking a look at a more complicated example, let’s see how we can represent our gallery app using a finite state machine. The app can be in one of several states:

  • start – the initial search page view
  • loading – search results fetching view
  • error – search failed view
  • gallery – successful search results view
  • photo – detailed single photo view

And several actions can be performed, either by the user or the app itself:

  • SEARCH – user clicks the “search” button
  • SEARCH_SUCCESS – search succeeded with the queried photos
  • SEARCH_FAILURE – search failed due to an error
  • CANCEL_SEARCH – user clicks the “cancel search” button
  • SELECT_PHOTO – user clicks a photo in the gallery
  • EXIT_PHOTO – user clicks to exit the detailed photo view

The best way to visualize how these states and actions come together, at first, is with two very powerful tools: pencil and paper. Draw arrows between the states, and label the arrows with actions that cause transitions between the states:

We can now represent these transitions in an object, just like in the traffic light example:

const galleryMachine = {
  start: {
    SEARCH: 'loading'
  },
  loading: {
    SEARCH_SUCCESS: 'gallery',
    SEARCH_FAILURE: 'error',
    CANCEL_SEARCH: 'gallery'
  },
  error: {
    SEARCH: 'loading'
  },
  gallery: {
    SEARCH: 'loading',
    SELECT_PHOTO: 'photo'
  },
  photo: {
    EXIT_PHOTO: 'gallery'
  }
};

const initialState = 'start';

Now let’s see how we can incorporate this finite state machine configuration and the transition function into our gallery app. In the App‘s component state, there will be a single property that will indicate the current finite state, gallery:

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      gallery: 'start', // initial finite state
      query: '',
      items: []
    };
  }
  // ...

The transition function will be a method of this App class, so that we can retrieve the current finite state:

  // ...
  transition(action) {
    const currentGalleryState = this.state.gallery;
    const nextGalleryState =
      galleryMachine[currentGalleryState][action.type];

    if (nextGalleryState) {
      const nextState = this.command(nextGalleryState, action);

      this.setState({
        gallery: nextGalleryState,
        ...nextState // extended state
      });
    }
  }
  // ...

This looks similar to the previously described transition(currentState, action) function, with a few differences:

  • The action is an object with a type property that specifies the string action type, e.g., type: 'SEARCH'
  • Only the action is passed in since we can retrieve the current finite state from this.state.gallery
  • The entire app state will be updated with the next finite state, i.e., nextGalleryState, as well as any extended state (nextState) that results from executing a command based on the next state and action payload (see the “Executing commands” section)

Executing commands

When a state change occurs, “side effects” (or “commands” as we’ll refer to them) might be executed. For example, when a user clicks the “Search” button and a 'SEARCH' action is emitted, the state will transition to 'loading', and an async Flickr search should be executed (otherwise, 'loading' would be a lie, and developers should never lie).

We can handle these side effects in a command(nextState, action) method that determines what to execute given the next finite state and action payload, as well as what the extended state should be:

  // ...
  command(nextState, action) {
    switch (nextState) {
      case 'loading':
        // execute the search command
        this.search(action.query);
        break;
      case 'gallery':
        if (action.items) {
          // update the state with the found items
          return { items: action.items };
        }
        break;
      case 'photo':
        if (action.item) {
          // update the state with the selected photo item
          return { photo: action.item };
        }
        break;
      default:
        break;
    }
  }
  // ...

Actions can have payloads other than the action’s type, which the app state might need to be updated with. For example, when a 'SEARCH' action succeeds, a 'SEARCH_SUCCESS' action can be emitted with the items from the search result:

    // ...
    fetchJsonp(
      `https://api.flickr.com/services/feeds/photos_public.gne?lang=en-us&format=json&tags=${encodedQuery}`,
      { jsonpCallback: 'jsoncallback' })
      .then(res => res.json())
      .then(data => {
        this.transition({ type: 'SEARCH_SUCCESS', items: data.items });
      })
      .catch(error => {
        this.transition({ type: 'SEARCH_FAILURE' });
      });
    // ...

The command() method above will immediately return any extended state (i.e., state other than the finite state) that this.state should be updated with in this.setState(...), along with the finite state change.

The final machine-controlled app

Since we’ve declaratively configured the finite state machine for the app, we can render the proper UI in a cleaner way by conditionally rendering based on the current finite state:

  // ...
  render() {
    const galleryState = this.state.gallery;

    return (
      <div className="ui-app" data-state={galleryState}>
        {this.renderForm(galleryState)}
        {this.renderGallery(galleryState)}
        {this.renderPhoto(galleryState)}
      </div>
    );
  }
  // ...

The final result:

See the Pen Gallery app with Finite State Machines by David Khourshid (@davidkpiano) on CodePen.

Finite state in CSS

You might have noticed data-state={galleryState} in the code above. By setting that data-attribute, we can conditionally style any part of our app using an attribute selector:

.ui-app {
  // ...
  
  &[data-state="start"] {
    justify-content: center;
  }
  
  &[data-state="loading"] {
    .ui-item {
      opacity: .5;
    }
  }
}

This is preferable to using className because you can enforce the constraint that only a single value at a time can be set for data-state, and the specificity is the same as using a class. Attribute selectors are also supported in most popular CSS-in-JS solutions.

Advantages and resources

Using finite state machines for describing the behavior of complex applications is nothing new. Traditionally, this was done with switch and goto statements, but by describing finite state machines as a declarative mapping between states, actions, and next states, you can use that data to visualize the state transitions:

Gallery app state transition diagram

Furthermore, using declarative finite state machines allows you to:

  • Store, share, and configure application logic anywhere – similar components, other apps, in databases, in other languages, etc.
  • Make collaboration easier with designers and project managers
  • Statically analyze and optimize state transitions, including states that are impossible to reach
  • Easily change application logic without fear
  • Automate integration tests

Conclusion and takeaways

Finite state machines are an abstraction for modeling the parts of your app that can be represented as finite states, and almost all apps have those parts. The FSM coding patterns presented in this article:

  • Can be used with any existing state management setup; e.g., Redux or MobX
  • Can be adapted to any framework (not just React), or no framework at all
  • Are not written in stone; the developer can adapt the patterns to their coding style
  • Are not applicable to every single situation or use-case

From now on, when you encounter “boolean flag” variables such as isLoaded or isSuccess, I encourage you to stop and think about how your app state can be modeled as a finite state machine instead. That way, you can refactor your app to represent state as state === 'loaded' or state === 'success', using enumerated states in place of boolean flags.

Resources

I gave a talk at React Rally 2017 about using finite automata and statecharts to create better user interfaces, if you want to learn more about the motivation and principles:

Slides: Infinitely Better UIs with Finite Automata

Here are some further resources: