Skip to content
10 minute read

“Just Use Hooks”: XState in React Components

Kevin Maes

Are you a React developer using XState to model your application logic? Perhaps you’ve heard of XState but have been looking for an easy way to try it out in one of your projects. If so, then I’d like to share with you a pattern I was introduced to when first diving into codebase at Stately, that of using custom machine hooks. This lightweight, reusable way to integrate XState into React components is a delight to work with and I think you might like it as much as I do!

Introduction

In this post I’ll review the most common way to use the @xstate/react library in a project. I’ll then demonstrate how encapsulation and reuse of state machines can be achieved by using hooks in your components, with some examples. I’ll also touch on the advantages and disadvantages to using this level of abstraction.

For more background, you can check out “Just Use Props”: An opinionated guide to React and XState by Matt Pocock.

After years of usage in the wild, and in response to confusion and frustration about hooks, the React Team has been putting a lot of effort into making the use of hooks clearer and simpler. Now is the perfect time to re-explore how hooks, when used effectively, can help make component creation easier.

XState in React components

For those of you who are already using XState with React, you’re probably used to creating a machine using createMachine() and then passing that machine to the useMachine hook from within a component.

Here is the code from the Quick Start example in the @xstate/react docs, where a toggleMachine (view in the Editor) is created with createMachine() and then passed to useMachine() for use in a Toggler component.

import { useMachine } from "@xstate/react";
import { createMachine } from "xstate";

const toggleMachine = createMachine({
id: "toggle",
initial: "inactive",
states: {
inactive: {
on: { TOGGLE: "active" },
},
active: {
on: { TOGGLE: "inactive" },
},
},
});

export const Toggler = () => {
const [state, send] = useMachine(toggleMachine);

return (
<button onClick={() => send("TOGGLE")}>
{state.value === "inactive"
? "Click to activate"
: "Active! Click to deactivate"}
</button>
);
};

This example shows how you can then evaluate state.value to render the corresponding UI for the toggle state and you can also call send('TOGGLE') in a button’s onClick handler to toggle the state.

Similarly, one could access other state methods and properties like state.matches(), state.can(), state.hasTag(), or even state.context to evaluate state and show the correct UI.

A custom machine hook

But what if your component really doesn’t need access to all of those features when using a machine? That’s where the custom machine hook comes into play. We can still create a machine and pass it to the useMachine hook but this can all be done inside of the custom hook.

Here’s an example of what that custom hook might look like, including a React/TypeScript CodeSandbox version:

import { useMachine } from "@xstate/react";
import { useEffect } from "react";
import { createMachine } from "xstate";

export const useToggleMachine = (
initialActive: boolean = false
): [boolean, () => void] => {
const [state, send] = useMachine(() =>
createMachine({
id: "toggle",
initial: initialActive ? "active" : "inactive",
states: {
inactive: {
on: { TOGGLE: "active" },
},
active: {
on: { TOGGLE: "inactive" },
},
},
})
);

const isActive = state.matches("active");
const toggle = () => send("TOGGLE");

return [isActive, toggle];
};

Why a hook?

Why might we opt for this extra layer of abstraction? Well, consider what we need to do in the example:

  1. Create the toggleMachine, including states and possible transitions.
  2. Pass the machine to the useMachine hook.
  3. Expose the most relevant pieces to our component.

From this hook, we can expose a minimal interface to components. In fact, we don’t even need to export the machine at all. By encapsulating the XState code, we allow components to focus on their core task, rendering UI as a function of data/props. Sweet!

Our useToggleMachine hook now fully manages a toggle state for any component that uses it. This is now more resuable since a single component can create multiple instances of useToggleMachine. Similarly, multiple components can instantiate this hook one or more times to keep track of multiple, separate toggle states.

Related: If you’re wondering about how to create a global machine hook then have a look at this RFC for a Global Hooks API.

Let’s dive deeper into a the details of this useToggleMachine hook.

Hook params

If you recall, we initialized our machine to start out in its “inactive” state by specifying initial: 'inactive' in the machines config object. But we’re also receiving an initialActive value as the one and only argument passed into this hook. If that value is false or omitted, since it defaults to false, then the inital value will be in sync with the machine’s default state.

But what if we want to start out with initialActive passed in as true? We need a way to immediately transiton our machine away from its own initial state to be synchronized with the incoming initialActive value.

The original version of this post included an example that used the infamous useEffect hook to dynamically establish the initial state, based on the initialActive prop passed into the hook.

// This example is deprecated
useEffect(() => {
if (initialActive && state.matches("inactive")) {
send("TOGGLE");
}
}, [initialActive]);

Some readers noted in their feedback that they prefer to avoid using the useEffect hook altogether for understandable reasons. The state really should be included in the dependency array to make the linter happy and using useEffect here feels generally awkward.

Instead, I've updated the example by wrapping the call to createMachine in a function passed to useMachine, utilizing a sort of "lazily created machine".

const [state, send] = useMachine(() =>
createMachine({
id: "toggle",
initial: initialActive ? "active" : "inactive",
states: {
inactive: {
on: { TOGGLE: "active" },
},
active: {
on: { TOGGLE: "inactive" },
},
},
})
);

This gives our machine config object access to the incoming initialActive prop so that we can dynamically assign the machine's initial value. It's a subtle but significant change.

initial: initialActive ? "active" : "inactive",

You can read about alternative methods and proposals in our RFC for input.

Return values

We’ve looked at the input param for useToggleMachine so now let’s look at its return values.

const isActive = state.matches("active");
const toggle = () => send("TOGGLE");

return [isActive, toggle];

We have a boolean isActive value which is derived from the state of the machine, the raison d’être of this hook. This is a simple mapping of one of two machine states to a boolean in this example. But you can imagine how states of a more complex machine might be derived from evaluating matches on the current state, possible next events, and even tags. Vist the docs on state methods for details.

We also have a toggle function which enables us to toggle the state of the machine. It’s an anonymous function wrapping the call to XState’s send('TOGGLE').

Our hook returns an array of just these values much like useState or useMachine would and they should be destructured in the component.

Using the hook in a component

What does this look like for the Toggler component to now use our useToggleMachine hook? It looks pretty good!

const Toggler = () => {
const [isActive, toggle] = useToggleMachine(false); // Or pass true.

return <button onClick={toggle}>Click me ({isActive ? "✅" : "❌"})</button>;
};

In that example, we use the value of isActive to specify the button’s text but it could easily be used for other purposes in this component or as a prop to pass down to child components.

For the Toggler component’s onChange handler, we set its value to be the toggle function. Since that is already wrapping XState’s send('TOGGLE') call, we don’t even need to use another anonymous function. It all just works as is, in a tidy functional style.

Reusability

As you can see, this pattern separates our normal React component code from our state machine implementation which keeps files neat and focused. Hooks make for more reusable machines across many components and in different situations. A useToggleMachine may be used to represent a toggle switch in one component but it might also represent the showing or hiding of UI or something else in another component.

const [isAnimationEnabled, toggleAnimation] = useToggleMachine(false);

const [isDarkMode, toggleLightDarkMode] = useToggleMachine(true);

In a future blog post we can explore ways to compose machine hooks to build up more sophisticated machines from reusable parts, not unlike how small reusable functions are typically composed to create larger functions.

Team specialization

This separation of code also means that team members who are more familiar with XState can create and manage machine hooks with autonomy. Meanwhile, their teammates, who may be less familiar with state machines or with XState, can still rapidly churn out UI components that will, nevertheless, be backed by the power of state machines. This greatly facilitates incremental adoption. You can begin using XState in small bits and pieces right away, neither needing to design your entire application as a large statechart nor rewrite everything to fit that way of working.

Caveats

If you only have one component and all you need to do is toggle a boolean flag, then creating a machine and a hook on top of that may feel like unnecessary ceremony. Splitting code into two different files has the usual tradeoffs. Also, understanding how changing the initialActive prop works with the state machine’s own internal state can be a bit tricky although we’d still need to transition the machine to a non-initial state, in a similar way, from within a component that calls useMachine().

Summary

We saw a baseline example of how components traditionally employ the useMachine hook from XState/react with a complete example of how to separate the machine into its own custom useToggleMachine hook for comparison. We covered implementation details for this hook, as well as how to wire it up in a React component. I’ve offered several benefits that I believe make this abstraction worthwhile like incremental adoption and future feature scaling.

Next steps

Again, the toggle example is a small yet usable example for creating a machine with XState and wrapping it in a hook. But we can take this even further. What about combining multiple machines into a single hook? How about overriding machine implmentation details via hooks on a per use basis? I’ll be exploring these patterns and more in some upcoming blog posts so stay tuned!

In the meantime, if you like using XState then keep creating your own machines and try wrapping those in custom hooks to use in your components. Additonally, you can build upon machine/hook examples in these posts for your own purposes and even find machines in the Discovery section of the Stately Studio and turn those into hooks. Whatever path you take, I hope you get hooked on using XState to make your UI more robust and more reusable!