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:

paused: Video playback is currently paused.
playing: Video is currently playing.
ended: The video has reached the end of the track.

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.

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",
states: {
on: {
actions: ["setVideo"]
},
FAIL: "failure"
}
}
}
});
``````

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:

``````{
"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.

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={() => {
}}
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 =>
) && (
<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",

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

states: {
on: {
actions: ["setVideo"]
},
FAIL: "failure"
}
},
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.