Data Fetching with Error Recovery
Problem
You need to fetch remote data, show loading state while the request is in flight, handle failures with a retry path, and automatically cancel in-flight requests when the state changes.
Solution
Use an invoke array in the loading state. The runtime passes an AbortSignal to src and cancels it when the state is exited, preventing stale responses from updating context.
ts
import { machine } from '@vielzeug/clockwork';
type State = 'error' | 'idle' | 'loading';
type Context = { data: string[]; error: string };
type Event =
| { type: 'FETCH' }
| { type: 'RETRY' }
| { items: string[]; type: 'SUCCESS' }
| { message: string; type: 'FAILURE' };
const fetcher = machine({
context: { data: [], error: '' },
initial: 'idle',
states: {
error: {
on: { RETRY: { target: 'loading' } },
},
idle: {
on: { FETCH: { target: 'loading' } },
},
loading: {
invoke: [
{
onDone: (items, _ctx) => ({ items: items as string[], type: 'SUCCESS' }),
onError: (err, _ctx) => ({ message: String(err), type: 'FAILURE' }),
src: async ({ signal }) => {
const res = await fetch('/api/items', { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
},
],
on: {
FAILURE: {
actions: [
({ context, event }) => {
context.error = event.message;
},
],
target: 'error',
},
SUCCESS: {
actions: [
({ context, event }) => {
context.data = event.items;
context.error = '';
},
],
target: 'idle',
},
},
},
},
});
m.send({ type: 'FETCH' }); // enters loading, invoke starts
// The AbortSignal is cancelled automatically if the state exits before the response arrivesPitfalls
onDonereceives the raw resolved value. The return type ofsrcisPromise<unknown>, so cast the result insideonDonebefore building the event object.FAILUREandSUCCESSevents must be defined inon. If they are missing, the dispatched event is silently ignored, leaving the machine stuck inloading.- Multiple invokes race. If you add more than one entry to the
invokearray, all start concurrently and whichever resolves first dispatches. Use a single invoke unless you intentionally want a race. signalis only valid insidesrc. Do not pass it toonDoneoronError— those only receive the result/error.- Context in
onDone/onErrorargs is captured at invoke start. This prevents stale-context bugs when the machine transitions between invoke creation and resolution.
Related
- Auth Flow with Guards — Async invoke with guard-controlled retry
- Persisted Wizard — Persisting context across sessions
- API Reference —
InvokeDef