useActor hook in React components

@xstate/react provides a hook called useActor that allows us to connect an actor to a component.

The useActor hook accepts an actor and returns an array with two values:

  • snapshot: contains the current context and status of the actor
  • send: function that allows us to send events to the actor
import { useActor } from "@xstate/react";
import { fromTransition } from "xstate";

type Event = { type: "toggle" };
type Context = boolean;
const initialContext = false;

const actor = fromTransition((context: Context, event: Event) => {
  if (event.type === "toggle") {
    return !context;
  }

  return context;
}, initialContext);

export default function Actor() {
  const [snapshot, send] = useActor(actor);
  return (<></>);
}

snapshot and send are two common naming conventions used for the result of react hooks in XState.

We don't use the name context because snapshot contains more than just the context value.

send instead works the same as dispatch in useReducer.

The UI code is nearly the exact same as useReducer:

import { useActor } from "@xstate/react";
import { fromTransition } from "xstate";

type Event = { type: "toggle" };
type Context = boolean;
const initialContext = false;

const actor = fromTransition((context: Context, event: Event) => {
  if (event.type === "toggle") {
    return !context;
  }

  return context;
}, initialContext);

export default function Actor() {
  const [snapshot, send] = useActor(actor);
  return (
    <input
      type="checkbox"
      checked={snapshot.context}
      onChange={() => send({ type: "toggle" })}
    />
  );
}

That's all!

As you can see, the code is almost identical to the useReducer example.

We can even share the same reducer function. It's clear how the only difference is where initialContext is provided:

import { useActor } from "@xstate/react";
import { fromTransition } from "xstate";

type Event = { type: "toggle" };
type Context = boolean;
const initialContext = false;

const reducer = (context: Context, event: Event): Context => {
  if (event.type === "toggle") {
    return !context;
  }
  return context;
};

// 👆 All shared code


// 👇 With `useReducer`
function UseReducer() {
  const [context, dispatch] = useReducer(reducer, initialContext);
  return (
    <input
      type="checkbox"
      checked={context}
      onChange={() => dispatch({ type: "toggle" })}
    />
  );
}


// 👇 With `useActor`
const actor = fromTransition(reducer, initialContext);
function Actor() {
  const [snapshot, send] = useActor(actor);
  return (
    <input
      type="checkbox"
      checked={snapshot.context}
      onChange={() => send({ type: "toggle" })}
    />
  );
}

XState is not necessarily more complex than normal react hooks.

You can create multiple types of actors, sometimes as simple as fromTransition for use cases similar to useReducer.

Again, state machines are more complex and complete, but they are not necessary for every use case.

The key building blocks that you will find in all actors are:

  • Event-driven programming: actors communicate by sending and receiving events
  • Internal state: actors have their own state, not accessible from the outside