No more useEffect: Invoke actor in initial state

We use all the concepts we learned in the previous modules to implement the searching logic with XState.

In practice, we are going to implement the following:

  1. Actor that performs async request to fetch posts
  2. State machine (actor) responsible to manage the current state of the app ("searching" state and post list)

These two actors are implemented in isolation. The state machine is then going to invoke the fetch posts actor and wait for the request to complete.

This is how the final model looks like:

The main state machines enters the "Searching" state. From here it invokes the searching actor and wait for the output. When the searching actor is done the machine transitions to the "Idle" state from "onDone"
The main state machines enters the "Searching" state. From here it invokes the searching actor and wait for the output. When the searching actor is done the machine transitions to the "Idle" state from "onDone"

First we add Context and Event in types together with the initialContext:

import { setup } from "xstate";
import { initialContext, type Context } from "./shared";

type Event =
  | { type: "update-query"; value: string }
  | { type: "submit-search" };

const machine = setup({
  types: {
    events: {} as Event,
    context: {} as Context,
  },
}).createMachine({
  context: initialContext,
});

Since we perform a search request as soon as the component loads, we define initial as "Searching":

const machine = setup({
  types: {
    events: {} as Event,
    context: {} as Context,
  },
}).createMachine({
  context: initialContext,
  initial: "Searching",
  states: {
    Searching: {}
  }
});

Invoking actor in initial state

An async request is implemented inside an actor with fromPromise as we did in the previous module. The actor requires as input the query to search for:

import { fromPromise } from "xstate";

const searchingActor = fromPromise(
  async ({ input }: { input: { query: string } }): Promise<Post[]> =>
    searchRequest(input.query)
);

We add the searchingActor inside setup/actors:

const machine = setup({
  types: {
    events: {} as Event,
    context: {} as Context,
  },
  actors: { searchingActor },
}).createMachine({
  context: initialContext,
  initial: "Searching",
  states: {
    Searching: {}
  }
});

We can now invoke the searchingActor inside the "Searching" state passing the query as input from context

const machine = setup({
  types: {
    events: {} as Event,
    context: {} as Context,
  },
  actors: { searchingActor },
}).createMachine({
  context: initialContext,
  initial: "Searching",
  states: {
    Searching: {
      invoke: {
        src: "searchingActor",
        input: ({ context }) => ({ query: context.query })
      }
    }
  }
});

The key here is that "Searching" is the initial state, and the invoke action is executed when the state is entered.

Therefore, adding invoke in the initial state allows to perform a request "on mount".

This behaves the same as useEffect with an empty dependency array, but without all the drawbacks of useEffect.