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 thesubmit
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 returnstrue
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>
);
}
We extracted all the logic from the component and moved it inside two actors:
submitActor
: Executes the async requestmachine
: 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.