Extract promise actor output

The rest of the implementation uses all the same concepts we learned previously.

When the searchingActor completes successfully, we extract the posts from onDone by accessing event.output and transition to a new "Idle" state:

The result returned from a fromPromise actor can be accessed inside onDone as event.output.

The type of output is inferred from the actor return type (Post[]).

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 }),
        onDone: {
          target: "Idle",
          actions: assign(({ event }) => ({
            // 👇 `event.output` contains result of `searchingActor`
            posts: event.output,
          })),
        },
      },
    },
    Idle: {},
  },
});

Remember that actions can be executed with every state transition. In this case we add the onUpdatePosts action when transitioning from "Searching" to "Idle" inside onDone.

Inside "Idle" we add a on event handler to update the query and submit the search.

After the submit event we transition to "Searching". This will trigger again invoke, which will perform the request with the new query:

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 }),
        onDone: {
          target: "Idle",
          actions: assign(({ event }) => ({
            posts: event.output,
          })),
        },
      },
    },
    Idle: {
      on: {
        "update-query": {
          actions: assign(({ event }) => ({
            query: event.value,
          })),
        },
        "submit-search": {
          target: "Searching",
        },
      },
    },
  },
});

The component looks the same as useState and useReducer, but without useEffect or any other logic inside it:

export default function Machine() {
  const [snapshot, send] = useMachine(machine);
  return (
    <div>
      <div>
        <input
          type="search"
          value={snapshot.context.query}
          onChange={(e) =>
            send({ type: "update-query", value: e.target.value })
          }
        />
        <button type="button" onClick={() => send({ type: "submit-search" })}>
          Search
        </button>
      </div>

      {snapshot.context.posts.map((post) => (
        <div key={post.id}>
          <p>{post.title}</p>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
}

XState extracts side effect management from the component. All the logic is contained inside actors.


This example shows how XState can implement many patterns by combining actors and state machines.

Notice how we did not introduce any new concept, we simply used the same features we learned in the previous modules (actors, actions, states).

Nonetheless, we were able to implement a new pattern by understanding how machines are executed.

Every time we enter "Searching" a new request is performed using the current query from context. This pattern make state management more declarative, since we don't need to manually trigger requests every time.

It also allows avoiding useEffect completely, making the logic more predictable.