Implementing the React component

Once again, the component code is minimal. We moved all the logic outside of the component, therefore the component only defines the UI and sends events to the machine:

  • onSubmit sends the submit event
  • Mark the button as disabled when the machine matches the "Loading" state
export default function Machine() {
  const [snapshot, send] = useActor(machine);
  return (
    <form
      onSubmit={(event) => send({ type: "submit", event })}
    >
      <input
        type="text"
        value={snapshot.context.username}
        onChange={(e) =>
          send({ type: "update-username", username: e.target.value })
        }
      />
      <button
        type="submit"
        disabled={snapshot.matches("Loading")}
      >
        Confirm
      </button>
    </form>
  );
}

snapshot.matches("Loading") is a helper function that returns true when the machine is in the "Loading" state.

snapshot.matches is used to check the current state.

In practice, it works the same as snapshot.value === "Loading".


This is the full code:

import { useActor } from "@xstate/react";
import { assign, fromPromise, setup } from "xstate";
import { initialContext, postRequest, type Context } from "./shared";

type Event =
  | { type: "update-username"; username: string }
  | { type: "submit"; event: React.FormEvent<HTMLFormElement> };

/// 👇 Independent actor for async request
const submitActor = fromPromise(async ({ input }: {
    input: { event: React.FormEvent<HTMLFormElement>; context: Context };
  }) => {
    input.event.preventDefault();
    await postRequest(input.context);
  }
);

// 👇 Main actor (state machine) that manages context and state
const machine = setup({
  types: {
    context: {} as Context,
    events: {} as Event,
  },
  actors: { submitActor },
}).createMachine({
  context: initialContext,
  initial: "Editing",
  states: {
    // 👇 State when the user is editing the form
    Editing: {
      on: {
        "update-username": {
          actions: assign(({ event }) => ({
            username: event.username,
          })),
        },
        submit: { target: "Loading" },
      },
    },

    // 👇 State when the form is being submitted
    Loading: {
      invoke: {
        src: "submitActor",
        input: ({ event, context }) => {
          assertEvent(event, "submit");
          return { event: event.event, context };
        },
        onDone: { target: "Complete" },
      },
    },

    // 👇 State when the async request completed successfully
    Complete: {},
  },
});

export default function Machine() {
  const [snapshot, send] = useActor(machine);
  return (
    <form onSubmit={(event) => send({ type: "submit", event })}>
      <input
        type="text"
        value={snapshot.context.username}
        onChange={(e) =>
          send({ type: "update-username", username: e.target.value })
        }
      />
      <button type="submit" disabled={snapshot.matches("Loading")}>
        Confirm
      </button>
    </form>
  );
}
Github Repository

We extracted all the logic from the component and moved it inside two actors:

  • submitActor: Executes the async request
  • machine: Manages the state and context of the form

We are now in a position to compare the implementations using useState, useReducer and XState. Let's do this in the next lesson.