0

I started playing with xstate and decided to start with making a fetch machine that will be responsible for every request I make to the server.

I created a simple state machine:

import { createMachine, assign } from "xstate";

interface FetchContext {
  readonly results: any | null;
  readonly error: null | string;
}

type FetchServices = {
  readonly fetchData: {
    readonly data: any;
  };
}

export const fetchMachine = createMachine(
  {
    context: {
      results: null,
      error: null,
    },
    tsTypes: {} as import("./fetch.typegen").Typegen0,
    schema: {
      context: {} as FetchContext,
      services: {} as FetchServices,
    },
    id: "fetch",
    initial: "idle",
    states: {
      idle: {
        on: {
          FETCH: {
            target: "pending",
          },
        },
      },
      pending: {
        invoke: {
          src: "fetchData",
          onDone: { target: "success", actions: "setResults" },
          onError: { target: "failed", actions: "setError" },
        },
      },
      success: {
        on: {
          FETCH: {
            target: "pending",
          },
        },
      },
      failed: {
        on: {
          FETCH: {
            target: "pending",
          },
        },
      },
    },
  },
  {
    actions: {
      setResults: assign((context, event) => {
        return {
          results: event.data,
          error: null,
        };
      }),
      setError: assign((context, event) => {
        return {
          results: null,
          error: event.data as string,
        };
      }),
    },
  }
);

The main problem here is that I want this fetchMachine to be responsible for different requests with different return types. As you can see fetched data type in my code is "any" and I want to fix that. If I used the fetchMachine just for one request, I would describe returned object type and the problem would be gone, but in my case I want this fetchMachine to be reused for many different services.

The second problem (not related to services) is that if I remove "as string" from "setError" action's returned propery "error", typescript complains that "event.data" is unknown type and can not be assigned to "string | null" as I described error type in FetchContext interface. Is there a way to properly type errors in this case?

Also, is it a good practice to have a single fetching machine for every "get" request in state machines? I mean, sometimes I used to create a useFetch hook to handle most of the requests, that's why I'm asking.

1 Answers1

0

Regarding the main problem, I would go with factory function that accepts a generic:

export const createFetchMachine = <T>() =>
  createMachine(
    {
      context: {
        results: null,
        error: null,
      },
      tsTypes: {} as import("./test.typegen").Typegen0,
      schema: {
        context: {} as {
          results: T | null;
          error: null | string;
        },
        services: {} as {
          fetchData: {
            data: T;
          };
        },
      },
      id: "fetch",

About the second problem, you can read more about here. When using typegen, you should cast your error type as you did in your example.

As to the generic fetch machine, that was the first thing I did when I started using xState, but with time I found out that simple invoke promises worked better and cleaner for me.

z_lander
  • 104
  • 1
  • 8