The key feature of XState compared to useState
and useReducer
is that async requests are defined inside the machine, and not in the component.
This allows to extract the complete logic from the component, keeping the component pure, only handling the UI and sending events.
In XState any combination of effects is implemented by combining independent actors.
Executing an async request therefore requires to define a separate actor. An actor can be derived from a function returning a Promise
by using fromPromise
:
import { fromPromise } from "xstate";
const submitActor = fromPromise(async () => {
// Implement the async request here
});
Notice how this is a completely different actor.
This separation allows focusing on each part of the logic in isolation. We will then combine each actor to compose the full app logic.
Passing input to fromPromise
Similar to what we did for actions, we can type the required input inside the fromPromise
function parameter. fromPromise
provides the actor metadata as parameters, included the input
:
We can type input
to make it required:
const submitActor = fromPromise(
async ({ input }: {
input: { event: React.FormEvent<HTMLFormElement>; context: Context };
}) => {
// TODO
}
);
This will require the input to be passed when executing the actor.
We can then proceed to implement the submitActor
:
import { fromPromise } from "xstate";
import { postRequest, type Context } from "./shared";
const submitActor = fromPromise(
async ({ input }: {
input: { event: React.FormEvent<HTMLFormElement>; context: Context };
}) => {
input.event.preventDefault();
await postRequest(input.context);
}
);
In this example all the logic is implemented inside the actor, included calling
preventDefault
. That's why the actor requires the fullReact.FormEvent
as input.Another option would be to call
preventDefault
inside the component and only pass thecontext
as input.