Raphael Ugwu Writer, Software Engineer and a lifelong student.

Deploying state machines with frontend frameworks

4 min read 1308

There are many ways to represent state in modern web applications. In React, you can either encapsulate your state locally within components or manage your state globally using a state management library like Redux. Whichever way the state of an app is represented, it is usually represented by components which can be changed to determine how the app is viewed.

When structuring apps, you may be required to have a stylesheet for each form of state your app will be in. Web apps have become increasingly more complex and creating a stylesheet for every form of state in your app could cause complications. In this blog post, we’ll learn how we can use finite state machines to create interactive and complex transitions in our apps.

When to use finite state machines

First off, finite state machines are models that explain the behavior of a system that can only be in one state at a point in time. A state machine’s job is to read a series of inputs and then for each input, switch to a different state. Let’s say you’ve got a car that’s represented by a state machine – it’s either moving, in a state of transition (slowly stopping or slowly starting) or stationary. It can’t be in all four states at the same time and it’s also impossible for it to be neither moving, transitioning between stopping and starting, transitioning between starting and stopping, nor stationary.

When structuring a frontend app, you would have to create events and conditions with event listeners and if or while expressions for each different state your app is in. The expansion of your app could see some expressions you make contradict each other. XState lets you implement a model for each state and handles transit between different states.

Working with XState

To show how XState works with frontend frameworks, we’ll build a concept of an interview page with XState and React. This page will render a question and a sidebar that gives a user the chance to peek at the question’s answer. At any given point in time, a user is either:

  • Viewing a question they’re about to answer
  • Viewing the answer to a question they do not understand
  • Transitioning from viewing a question to viewing an answer
  • Transitioning from viewing an answer to viewing a question

For animation, and including CSS styles in our components, we’ll make use of the following:

  • @emotion/core — A style composition library designed for writing CSS styles with JavaScript
  • gsap — A library for animating JavaScript objects

Assuming we’ve created a React app (see how to do that here), we’ll install both XState and xstate/react, a set of utilities for using XState in React apps:

npm i xstate @xstate/react

Creating our components

The first component we’ll create is a button for our transitions. In this button component, we’ll pass down props from our main component. Which will contain our state machine as well as our invoked functions:

// Button.jsx

/** @jsx jsx */
import { css, jsx } from "@emotion/core";
export const Button = props => (
  <button
    {...props}
  >
    {props.children}
  </button>
);

Next, we’ll create a Test component which will house the body of our app as well as functions that can be called by our state machine during its transition process. Let’s define two functions – openAnswer and closeAnswer for switching between the questions and answers:

// Test.jsx

const openAnswer = useCallback(
    (context, event) => {
      return new Promise(resolve => {
        TweenMax.to(element.current, 0.5, {
          x: 0,
          backdropFilter: "blur(2px)",
          ease: Elastic.easeOut.config(1, 1),
          onComplete: resolve
        });
      });
    },
    [element]
  );

  const closeAnswer = useCallback(
    (context, event) => {
      return new Promise(resolve => {
        TweenMax.to(element.current, 0.5, {
          x: -290,
          backdropFilter: "blur(0px)",
          ease: Elastic.easeOut.config(1, 1),
          onComplete: resolve
        });
      });
    },
    [element]
  );

In the code sample above, we used the useCallback Hook to keep track of changes and only update if a value has changed to prevent unnecessary re-renders. The image below depicts that state of the app when the Hook in openAnswer is triggered:

We made a custom demo for .
No really. Click here to check it out.

state of the app when the hook in openAnswer is triggered

And a similar image showing when the Hook in closeAnswer is triggered:

closeAnswer is triggered

Setting up our state machine and configuring our view

We’ll specify all four states our app can be in with the initial state as the view of an interview question:

<!--Test.jsx -->

const testMachine = Machine({
  initial: 'QUESTIONS',
  states: {
    QUESTIONS: {},
    VIEWING: {},
    ANSWERS: {},
    RETURNING: {},
  },
})

Next, we’ll configure in our state machine, the transition path we want our app to take. We’ll go from QUESTIONS to ANSWERS with VIEWING and RETURNING acting as transitions:

<!--Test.jsx -->

const testMachine = Machine({
  initial: 'QUESTIONS',
  states: {
    QUESTIONS: {
      on: {
        ANSWERS: 'VIEWING',
      },
    },
    VIEWING: {
      on: {
        REPEAT: 'RETURNING',
      },
    },
    ANSWERS: {
      on: {
        REPEAT: 'RETURNING',
      },
    },
    RETURNING: {
      on: {
       ANSWERS: 'VIEWING',
      },
    },
  },
})

To complete our state machine, we need to properly handle the transitions that occur in-between the VIEWING and RETURNING states. With its invoke property, XState has the ability to create functions that can return a promise. Let’s add the functions we earlier defined for switching between questions and answers:

<!--Test.jsx -->

const testMachine = Machine({
  initial: "QUESTIONS",
  states: {
    QUESTIONS: {
      on: {
        ANSWERS: "VIEWING"
      }
    },
    VIEWING: {
      invoke: {
        src: "openAnswer",
        onDone: { target: "ANSWERS" }
      },
      on: {
        REPEAT: "RETURNING"
      }
    },
    ANSWERS: {
      on: {
        REPEAT: "RETURNING"
      }
    },
    RETURNING: {
      invoke: {
        src: "closeAnswer",
        onDone: { target: "QUESTIONS" }
      },
      on: {
        ANSWERS: "VIEWING"
      }
    }
  }
});

Now we’re done defining our state, we’ll use the useMachine Hook to configure our state machine’s services. This exposes our app’s current state and the possible state it can be set to:

<!--Test.jsx -->

  const [current, send] = useMachine(testMachine, {
    services: {
      openAnswer,
      closeAnswer
    }
  });

Next, we need to set our user interface to communicate with the state and controls we’ve created so far. In Test.jsx, set a ternary operator to choose what the view should be on the main page when the ANSWERS button is clicked:

<!--Test.jsx -->

 const testAnswers =
 current.matches("ANSWERS") || current.matches("VIEWING") ? "REPEAT" : "ANSWERS";

 let label = testAnswers === "ANSWERS" ? "ANSWERS" : "REPEAT";

Next, we’ll create our button. When clicked, it should generate transitions that align with the ternary operator we just defined:

<!-- Test.jsx -->

return (
    <div
      ref={element}
      css={css`
        color: #fff;
        z-index: 9999;
        position: absolute;
        top: 0;
        bottom: 0;
        left: 0;
        width: 380px;
        transform: translateX(-290px);
        display: grid;
        grid-template-rows: 40px auto;
        align-content: start;
        justify-content: end;
      `}
    >
      <Button
        onClick={() => {
          send(testAnswers);
        }}
      >
        {label}
      </Button>
    </div>
  );

In the clip below, the header that shows the state of that app transitions between both states as the button – whose state also changes according to what we set in the ternary operator – is clicked:

Lastly, we’ll create a functional parent component that handles the multiple constants in our Test component and returns the starting state of our app via a useState Hook:

<!-- index.js -->

import { css, jsx } from "@emotion/core";
import { useState } from "react";
import { Test } from "./Test";

function App() {
  const [testStatus, setTestStatus] = useState();
  return (
    <div>
      <Test setStatus={setTestStatus} />
      <div>
          <div>
            {testStatus}
          </div>
          <div>
            <span role="img" aria-label="paper">
           📝
            </span>
            The inner diameter of a test tube can be measured accurately using a:
            <br></br>
        </div>
      </div>
    </div>
  );
}

We’re finally done! Here’s the finished app, note the simultaneous switching of states in all the parts involved:

Conclusion

There’s no uniform solution when it comes to handling state in web applications. Just be sure you’ve got your architecture right and the conditions you create in each component do not contradict. XState helps in making these conditions specific. Built around the concept of finite state machines which have been around for some time, it’s very stable. Should you like to understand the code samples more, you can view a full working demo here on CodeSandbox.

 

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Raphael Ugwu Writer, Software Engineer and a lifelong student.

Leave a Reply