fromPromise: actor from async request

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:

The function inside fromPromise gives access to various parameters, included the actor's input
The function inside fromPromise gives access to various parameters, included the actor's 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 full React.FormEvent as input.

Another option would be to call preventDefault inside the component and only pass the context as input.