ReactT Light_870x220

What are Finite State Machines and how can you use them in React to make complicated logic and UIs easier to grasp? In this article we’ll set out to provide an answer to this question by building a video player in React.

When I started to build the video player, I first thought about wanting to know if it was playing or paused. OK, I can use a boolean for that, right? But, while the video is loading, it’s not really playing or paused yet… it’s loading. Now I had two boolean values. What if it couldn’t load the video? What happens when it reaches the end of the video? You can see how something seemingly straightforward becomes harder to model.

Read on to see how XState by David K. Piano can help us model this complicated state in React, clearly defining the flow from one state to another.

The final version of the code referenced in this article can be found here.

What Is a Finite State Machine?

In the introduction I mentioned different “states” that our video player could be in:

  • loading: The initial state which occurs while we’re waiting for the video to load.
  • ready: Loading was successful.
    paused: Video playback is currently paused.
    playing: Video is currently playing.
    ended: The video has reached the end of the track.
  • failure: For whatever reason, the loading of the video failed.

I have listed six different states our video player can be in. Notice how it is a finite number (six), and not an infinite number of potential values? Now you know where the Finite of Finite State Machine comes from.

A Finite State Machine defines the possible states that our app (or portion of our app) can be in, and how it transitions from one state to another.

xstate-viz

 

What you’ve just seen above is the visual representation of the state machine for the video player we’ll be building.

Defining States and Transitioning Between Them

Let’s start to look at the code that defines the video state machine. It all starts with a large object that’s passed to Machine, where we define an id for the state machine, the initial state it should be in, followed by all the possible states.

const videoMachine = Machine({
  id: "video",
  initial: "loading",
  states: {
    loading: {
      on: {
        LOADED: {
          target: "ready",
          actions: ["setVideo"]
        },
        FAIL: "failure"
      }
    }
    // additional states
  }
});

You may have noticed that I only placed a single state here for now, called loading, and that’s so we can explain a few additional concepts before moving on. On the loading state we have an on attribute which is an object:

{
  "LOADED": {
    "target": "ready",
    "actions": ["setVideo"]
  },
  "FAIL": "failure"
}

This object defines all the possible events that the loading state is prepared to receive. In this case we have LOADED and FAIL. The LOADED event defines a target, which is the new state to be transitioned to when this event occurs. We also define some actions. These are side effects, or in simple terms, functions to call when this event occurs. More on these later.

The FAIL event is simpler, in that it simply transitions the state to failure, with no actions.

Context

Real-world applications aren’t made up of only finite states. In our video state machine, we actually have some additional data to keep track of, such as the duration of the video, how much time has elapsed, and a reference to the actual video HTML element.ReactT Light_270x123

In XState, this additional data is stored in the context.

const videoMachine = Machine({
  // ...
  context: {
    video: null,
    duration: 0,
    elapsed: 0
  },
  // ...
}

It starts out with some initial values, but we’ll see how to set and modify these values via actions below.

Events and Actions

Events are how to transition your state machine from one state to another. When using XState within a React app, you’ll most likely end up using the useMachine hook, which allows you to trigger events via the send function. In the below code we are triggering the LOADED event (which is available on the loading state), and we’ll pass some additional data to this event.

send("LOADED", { video: ref.current });

The send function in this case is called within the onCanPlay event which comes with the video element.

export default function App() {
  // Setup of ref to video element
  const ref = React.useRef(null);
  // Using the video state machine within React with useMachine hook
  const [current, send] = useMachine(videoMachine, {
    actions: { setVideo, setElapsed, playVideo, pauseVideo, restartVideo }
  });
  // Extract some values from the state machine context
  const { duration, elapsed } = current.context;

  return (
    <div className="container">
      <video
        ref={ref}
        onCanPlay={() => {
          send("LOADED", { video: ref.current });
        }}
        onTimeUpdate={() => {
          send("TIMING");
        }}
        onEnded={() => {
          send("END");
        }}
        onError={() => {
          send("FAIL");
        }}
      >
        <source src="/fox.mp4" type="video/mp4" />
      </video>

      {/* explanation of this code to come later */}
      {["paused", "playing", "ended"].some(subState =>
        current.matches({ ready: subState })
      ) && (
        <div>
          <ElapsedBar elapsed={elapsed} duration={duration} />
          <Buttons current={current} send={send} />
          <Timer elapsed={elapsed} duration={duration} />
        </div>
      )}
    </div>
  );
}

The setVideo action uses a function called assign from XState which allows you to update individual properties of the context. We’ll use this event as an opportunity to copy the ref to the video element over to the context, along with the video duration.

const setVideo = assign({
  video: (_context, event) => event.video,
  duration: (_context, event) => event.video.duration
});

Conditional Rendering Based on State Value

We’ve seen bits and pieces of the video state machine, but let’s take a look at it in its entirety. In the list of possible states, the ready state has three sub-states (paused, playing, ended), which is why you find it nested. This is referred to as hierarchical state nodes. In the state machine, we have defined all of the states, their events, and which actions are called for each event. If you’d like to refer back to the diagram to make sense of this, it is available here.

const videoMachine = Machine({
  id: "video",
  initial: "loading",

  context: {
    video: null,
    duration: 0,
    elapsed: 0
  },

  states: {
    loading: {
      on: {
        LOADED: {
          target: "ready",
          actions: ["setVideo"]
        },
        FAIL: "failure"
      }
    },
    ready: {
      initial: "paused",
      states: {
        paused: {
          on: {
            PLAY: {
              target: "playing",
              actions: ["setElapsed", "playVideo"]
            }
          }
        },
        playing: {
          on: {
            TIMING: {
              target: "playing",
              actions: "setElapsed"
            },
            PAUSE: {
              target: "paused",
              actions: ["setElapsed", "pauseVideo"]
            },
            END: "ended"
          }
        },
        ended: {
          on: {
            PLAY: {
              target: "playing",
              actions: "restartVideo"
            }
          }
        }
      }
    },
    failure: {
      type: "final"
    }
  }
});

Our video player should show the “Pause” button when the state is {ready: 'playing'}, and otherwise should be the “Play” button. Within the Buttons controller, we can control this using if statements along with the current.matches function. which allows us to match the current value of the state machine.

const Buttons = ({ current, send }) => {
  if (current.matches({ ready: "playing" })) {
    return (
      <button
        onClick={() => {
          send("PAUSE");
        }}
      >
        Pause
      </button>
    );
  }

  return (
    <button
      onClick={() => {
        send("PLAY");
      }}
    >
      Play
    </button>
  );
};

Conclusion

By thinking in terms of states and how our code transitions from one state to another via the events it receives, we’ve been able to model the complex logic of a video player in a way that makes it easier to reason about. If you’d like to hear more from David, the creator of the XState library, it’s worth listening to a podcast with Kent C. Dodds that he did recently, where they talk in detail about state machines and their relationship to music.


leigh-halliday
About the Author

Leigh Halliday

Leigh Halliday is a full-stack developer specializing in React and Ruby on Rails. He works for FlipGive, writes on his blog, and regularly posts coding tutorials on YouTube.

Related Posts

Comments

Comments are disabled in preview mode.