DEV Community

Cover image for Use an XState Machine with React
Josh Branchaud
Josh Branchaud

Posted on

Use an XState Machine with React

XState gives you the tools to take control over the state of your UI. When you've got it under control, you can build interfaces that provide a predictable and delightful user experience.

Let's look at how to integrate XState into a React app.

There are a bunch of well-constructed XState machines available to directly copy into your project from XState Catalogue. For instance, I can interact with and then grab the Confirmation Dialog machine with the 'Copy' button.

Alt Text

I'll then paste that machine definition into something like confirmMachine.js. XState is framework agnostic, so there is nothing about this machine, on its own, that has anything to do with React or Vue or Svelte or whatever. I do want to use this within a React app, so I then need to grab @xstate/react. XState's React "bindings" come with a useMachine hook.

An Example

Here is what that will look like.

import * as React from "react";
import { useMachine } from "@xstate/react";
import confirmMachine from "./confirmMachine";
import Dialog from "./dialog";

export default function App() {
  const [current, send] = useMachine(confirmMachine);

  return (
    <div className="App">
      <Dialog
        message="Are you sure you want to delete something?"
        {/* other props ... */}
      />
      {/* other stuff */}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The useMachine call both interprets and starts up the machine service. This hook gives you two values as an array. The current value is everything about the current state of the machine. The send is a function for dispatching transitions between machine states.

The Current State of the Machine

With current I can figure out the current state of the machine to determine whether or not I should be showing the dialog. current.value will tell me what state the machine is in.

I can also get access to any error message that comes from the machine.

import * as React from "react";
import { useMachine } from "@xstate/react";
import confirmMachine from "./confirmMachine";
import Dialog from "./dialog";

export default function App() {
  const [current, send] = useMachine(confirmMachine);

  const showDialog = current.value !== "closed";

  return (
    <div className="App">
      <Dialog
        message="Are you sure you want to delete something?"
        showDialog={showDialog}
        errorMessage={current.context.errorMessage}
      />
      {/* other stuff */}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Notice I check current.value !== "closed" to determine whether or not the dialog should be showing.

Moving Between States with Send

I can now incorporate the send function into some handlers so that users can interact with the dialog. I'll create a handler for opening, closing, and confirming the dialog.

import * as React from "react";
import { useMachine } from "@xstate/react";
import confirmMachine from "./confirmMachine";
import Dialog from "./dialog";

export default function App() {
  const [current, send] = useMachine(confirmMachine);

  const deleteAction = () => { /* ... */ };

  const showDialog = current.value !== "closed";
  const open = () => {
    send({ type: "OPEN_DIALOG", action: deleteAction });
  };
  const close = () => {
    send("CANCEL");
  };
  const confirm = () => {
    send("CONFIRM");
  };

  return (
    <div className="App">
      <Dialog
        message="Are you sure you want to delete something?"
        handleConfirm={confirm}
        handleClose={close}
        showDialog={showDialog}
        errorMessage={current.context.errorMessage}
      />
      {/* other stuff */}
      <button onClick={open}>Delete Something</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The open handler when called will transition the machine to open.idle using the OPEN_DIALOG event. It also includes an action which will be called if the dialog is confirmed. When triggered, this will cause the showDialog value to evaluate to true. This handler is wired up to some element outside of the dialog, in this case a button.

The close handler is passed to the dialog. When called this sends the CANCEL event to the machine. That will transition the machine back into the closed state. This change will cause the showDialog value to evaluate back to false. Any user action that should dismiss the dialog will trigger this handler.

Once the dialog is open, the user can confirm the dialog's prompt by clicking a 'Confirm' button. This will call the confirm handler which will send the CONFIRM event to the machine. When the machine receives this event it will trigger the action given on OPEN_DIALOG.

Wrapping Up

There are more details to this specific machine. Depending on whether the action's promise resolves or rejects, the machine will take a different course of action. That's an exercise for the reader or the subject of another post.

At this point, we have explored enough of XState in a React context that you can start using the two together. If you'd like you can start by interacting with and remixing the codesandbox example I used for this post.

There are a lot of moving parts when getting started with XState, so if you have questions about what was covered here, feel free to drop me a note on twitter.

If you enjoy my writing, consider joining my newsletter.


Cover photo by Ball Park Brand on Unsplash

Discussion (5)

Collapse
diasbruno profile image
Bruno Dias • Edited on

Great article.

Two good things about state machine are to make invalid state irrepressible and define only valid transitions.

There are a lot of applications for this, and specially on the frontend, this is a great tool to make code more reliable and testable.

Collapse
jbranchaud profile image
Josh Branchaud Author

What are some applications of state machines that you've found most helpful?

Collapse
diasbruno profile image
Bruno Dias • Edited on

Anything that can take 2, or more, distinct paths like wizard forms, processes...anything that looks like this...

Where each letter can be a component...

// React

const A = ({ state, setState, transformState }) => (
  <>
    <button ... onClick={() => setState([transformState(state, localState), B])}>Go to B</button>
    <button ... onClick={() => setState([transformState(state, localState), E])}>Go to E</button>
  </>
);

const B = ({ state, setState, transformState }) => (
  <button ... onClick={() => setState([transformState(state, localState), C])}>Go to C</button>
);

const C = ({ state, setState, transformState }) => (
  <button ... onClick={() => setState([transformState(state, localState), End])}>Go to End</button>
);

const D = ({ state, setState , transformState }) => (
  <button ... onClick={() => setState([transformState(state, localState), E])}>Go to E</button>
);

const E = ({ state, setState , transformState  }) => (
  <button ... onClick={() => setState([transformState(state, localState), End])}>Go to End</button>
);

const F = ({ state, setState , transformState  }) => (
  <button ... onClick={() => setState([transformState(state, localState), G])}>Go to G</button>
);

const G = ({ state, setState, transformState  }) => (
  <button ... onClick={() => setState([transformState(state, localState), End])}>Go to End</button>
);

const End = ({ state, setState }) => (
  <p>Fim!</p>
);

const DefaultStepComponent = ({ state, setState, transformState  }) => (
  <>
    <button ... onClick={() => setState([transformState(state), A])}>Go to A</button>
    <button ... onClick={() => setState([transformState(state), D])}>Go to D</button>
    <button ... onClick={() => setState([transformState(state), F])}>Go to F</button>
  </>
);

const Page = () => {
  const [[state, Step], setState] = useState([state, DefaultStepComponent]);
  // in Vue.js, you would use: 
  // <component :is="Step" ... />
  const transformState = {
    [DefaultStepComponent]: (state, localState) => { ... },
    ...
  };
  return (
    <Step state={state} setState={setState}  transformState={transformState[Step]} />
  );
};
Enter fullscreen mode Exit fullscreen mode
Collapse
robvirtuoso profile image
robvirtuoso

This is great, as long as people don't start using state machines for everything just because it's cool. 😎

Collapse
jbranchaud profile image
Josh Branchaud Author

Are there certain situations where you shouldn't use a state machine? Put another way, how do you know when is the right time to add state machines to an app?