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.
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>
)
}
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>
)
}
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>
)
}
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
Top comments (3)
What are some applications of state machines that you've found most helpful?
This is great, as long as people don't start using state machines for everything just because it's cool. π
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?