Skip to content

Data Fetching with Error Recovery

Problem

A request must show loading state, commit data on success, and offer a retry after failure.

Solution

Invoke the request in loading, then convert its settlement into events handled by the state map.

ts
import { defineMachine } from '@vielzeug/clockwork';

type Event =
  | { type: 'FETCH' }
  | { items: string[]; type: 'SUCCESS' }
  | { message: string; type: 'FAILURE' }
  | { type: 'RETRY' };

const fetcher = defineMachine<{ error: string; items: string[] }, Event>()({
  context: { error: '', items: [] },
  initial: 'idle',
  states: {
    idle: { on: { FETCH: { target: 'loading' } } },
    loading: {
      invoke: [{
        src: async ({ signal }) => {
          const response = await fetch('/api/items', { signal });
          if (!response.ok) throw new Error(`HTTP ${response.status}`);
          return response.json() as Promise<string[]>;
        },
        onDone: ({ result }) => ({ items: result as string[], type: 'SUCCESS' }),
        onError: ({ error }) => ({ message: String(error), type: 'FAILURE' }),
      }],
      on: {
        FAILURE: { reduce: ({ event }) => ({ error: event.message, items: [] }), target: 'error' },
        SUCCESS: { reduce: ({ event }) => ({ error: '', items: event.items }), target: 'ready' },
      },
    },
    ready: { on: { FETCH: { target: 'loading' } } },
    error: { on: { RETRY: { target: 'loading' } } },
  },
});

const actor = fetcher.createActor();
actor.subscribe((snapshot) => console.log(snapshot.state, snapshot.context));
actor.send({ type: 'FETCH' });

Pitfalls

  • Check response.ok; fetch() resolves for HTTP error responses.
  • Dispose the actor when the request owner unmounts to abort active work.