Using state machine in React component

Using the machine inside a component works the same as fromTransition.

In fact, since a state machine is itself an actor, we can use the useActor hook the same as we did with fromTransition:

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

type Event = { type: "toggle" };

const machine = setup({
  types: {
    events: {} as Event,
  },
}).createMachine({
  initial: "Off",
  states: {
    Off: {
      on: {
        toggle: { target: "On" },
      },
    },
    On: {
      on: {
        toggle: { target: "Off" },
      },
    },
  },
});

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

XState also has a useMachine hook, which is an alias for useActor specific to state machines.

The key difference is that we don't get a boolean value anymore, but instead the current state of the machine ("On" or "Off").

We extract the current state using snapshot.value:

The type of snapshot.value is inferred based on states defined in the machine. In this example it's "On" | "Off" as expected.

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

type Event = { type: "toggle" };

const machine = setup({
  types: {
    events: {} as Event,
  },
}).createMachine({
  initial: "Off",
  states: {
    Off: {
      on: {
        toggle: { target: "On" },
      },
    },
    On: {
      on: {
        toggle: { target: "Off" },
      },
    },
  },
});

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

Let's recap what we did in order:

  1. Define the states of the machine
  2. Define the initial state using initial
  3. Define Event with all possible events
  4. Define transitions for each state using on + target
  5. Use the machine inside a component using useActor
import { useActor } from "@xstate/react";
import { setup } from "xstate";

type Event = { type: "toggle" };

const machine = setup({
  types: {
    // 3️⃣ Define events
    events: {} as Event,
  },
}).createMachine({
  // 2️⃣ Define initial state
  initial: "Off",

  // 1️⃣ Define states
  states: {
    Off: {
      // 4️⃣ Define transitions
      on: {
        toggle: { target: "On" },
      },
    },
    On: {
      // 4️⃣ Define transitions
      on: {
        toggle: { target: "Off" },
      },
    },
  },
});

export default function Machine() {
  // 5️⃣ Use the machine inside a component with `useActor`
  const [snapshot, send] = useActor(machine);
  return (
    <input
      type="checkbox"
      checked={snapshot.value === "On"}
      onChange={() => send({ type: "toggle" })}
    />
  );
}

Whereas the UI code looks similar to the other examples, the logic is completely different from before.

That's because state machines are a different model of state management. Instead of working directly with a store (object), state machines work with states and transitions.

This is why XState is generally considered more complex than useReducer or useState.

We are going to appreciate the advantages of this state-based approach as we explore more complex use cases in the course.