Stately
XState v6 alpha

Handle async requests

Model loading, success and failure states for an async request.

Create async actor logic, then invoke it from a state.

import { createActor, createAsyncLogic, createMachine } from 'xstate';

const search = createAsyncLogic({
  run: async ({ input, signal }: { input: { query: string } }) => {
    const response = await fetch(`/api/search?q=${input.query}`, { signal });
    if (!response.ok) throw new Error('Search failed');
    return response.json() as Promise<string[]>;
  }
});

const searchMachine = createMachine({
  context: { query: '', results: [] as string[], error: null as unknown },
  initial: 'idle',
  states: {
    idle: {
      on: {
        search: ({ context, event }) => ({
          target: 'loading',
          context: { ...context, query: event.query }
        })
      }
    },
    loading: {
      invoke: {
        src: search,
        input: ({ context }) => ({ query: context.query }),
        onDone: ({ context, event }) => ({
          target: 'success',
          context: { ...context, results: event.output, error: null }
        }),
        onError: ({ context, event }) => ({
          target: 'failure',
          context: { ...context, error: event.error }
        })
      }
    },
    success: { on: { search: { target: 'loading' } } },
    failure: { on: { retry: { target: 'loading' } } }
  }
});

const actor = createActor(searchMachine).start();
actor.send({ type: 'search', query: 'actors' });

The request starts when the machine enters loading. It stops when the machine leaves loading.

The same shape works for loading a user profile or checking inventory before checkout. Keep request input in context when retry needs the same values.