Referencing an actor inside a state machine is done inside setup/actors
:
import { assign, fromPromise, setup } from "xstate";
import { initialContext, postRequest, type Context } from "./shared";
type Event = { type: "update-username"; username: string };
const submitActor = fromPromise(
async ({
input,
}: {
input: { event: React.FormEvent<HTMLFormElement>; context: Context };
}) => {
input.event.preventDefault();
await postRequest(input.context);
}
);
const machine = setup({
types: {
context: {} as Context,
events: {} as Event,
},
// 👇 Reference the actor inside `setup/actors`
actors: { submitActor },
}).createMachine({
context: initialContext,
initial: "Editing",
states: {
Editing: {
on: {
"update-username": {
actions: assign(({ event }) => ({
username: event.username,
})),
},
},
},
},
});
By doing this the actor can now be invoked inside any state.
"Invoking" an actor inside a state machine means that the actor will be executed when the state is entered.
We first add a new submit
event to the "Editing" state that transitions to a new "Loading" state:
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> };
const submitActor = fromPromise(
async ({
input,
}: {
input: { event: React.FormEvent<HTMLFormElement>; context: Context };
}) => {
input.event.preventDefault();
await postRequest(input.context);
}
);
const machine = setup({
types: {
context: {} as Context,
events: {} as Event,
},
actors: { submitActor },
}).createMachine({
context: initialContext,
initial: "Editing",
states: {
Editing: {
on: {
"update-username": {
actions: assign(({ event }) => ({
username: event.username,
})),
},
submit: { target: "Loading" },
},
},
Loading: {},
},
});
When the machine enters "Loading" we call invoke
to execute the submitActor
. We reference submitActor
inside the src
property:
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> };
const submitActor = fromPromise(
async ({
input,
}: {
input: { event: React.FormEvent<HTMLFormElement>; context: Context };
}) => {
input.event.preventDefault();
await postRequest(input.context);
}
);
const machine = setup({
types: {
context: {} as Context,
events: {} as Event,
},
actors: { submitActor },
}).createMachine({
context: initialContext,
initial: "Editing",
states: {
Editing: {
on: {
"update-username": {
actions: assign(({ event }) => ({
username: event.username,
})),
},
submit: { target: "Loading" },
},
},
Loading: {
invoke: {
src: "submitActor" // 👈 Reference the actor
},
},
},
});
invoke
will start submitActor
when the state is entered. In practice this means that entering "Loading" performs an async request.
This pattern of invoking actors is the core of the actor model.
By combining separate actors we can compose the full app logic, while implementing the logic in independent actors.
But wait, there is a type error! 🤔
Property 'input' is missing in type [...]
The required input
is missing. Let's fix this in the next lesson.