Invoking actor inside state machine

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 [...]

"invoke" reports a type error since a required input is missing.
"invoke" reports a type error since a required input is missing.

The required input is missing. Let's fix this in the next lesson.