Execute queries to local database

The last step in the state machine is to execute the queries to the local database.

Each request is implemented as a fromPromise actor, as we saw in the previous lesson:

export const machine = setup({
  // ...
  actors: {
    updatePlan: fromPromise(
      // ...
    ),
    removePlan: fromPromise(
      // ...
    ),
    setAsCurrentPlan: fromPromise(
      // ...
    ),
  },
}).createMachine({
  // ...
});

fromPromise requires to return a Promise that resolves with the result of the request.

Each request is composed by:

  • input to fromPromise actor
  • Extract and execute query from WriteApi service

We then convert the Effect to a Promise using RuntimeClient.runPromise:

export const machine = setup({
  // ...
  actors: {
    updatePlan: fromPromise(
      ({
        input,
      }: {
        // 👇 Typed input to actor
        input: {
          id: number;
          calories: number;
          fatsRatio: number;
          carbohydratesRatio: number;
          proteinsRatio: number;
        };
      }) =>
        // 👇 Execute effect (convert to `Promise`)
        RuntimeClient.runPromise(
          Effect.gen(function* () {
            // 👇 Extract query from `WriteApi` service
            const api = yield* WriteApi;
            yield* api.updatePlan(input);
          })
        )
    ),
    // ...
  },
}).createMachine({
  // ...
});

Invoke actors inside state machine

Executing the request is done inside invoke when entering the loading state:

export const machine = setup({
  // ...
}).createMachine({
  // ...
  states: {
    Idle: {
      on: {
        "plan.update": {
          target: "Updating",
        },
        "plan.remove": {
          target: "Removing",
        },
        "plan.set": {
          target: "Setting",
        },
      },
    },
    Updating: {
      invoke: {
        src: "updatePlan",
      },
    },
  },
});

We are required to provide a valid input. We get it by extracting the current snapshot from each field actor:

export const machine = setup({
  // ...
}).createMachine({
  // ...
  states: {
    Idle: {
      on: {
        "plan.update": {
          target: "Updating",
        },
        "plan.remove": {
          target: "Removing",
        },
        "plan.set": {
          target: "Setting",
        },
      },
    },
    Updating: {
      invoke: {
        src: "updatePlan",
        input: ({ event, context }) => {
          // 👇 Make sure the event is valid
          assertEvent(event, "plan.update");

          return {
            id: event.id,
            calories: context.calories.getSnapshot().context.value,
            fatsRatio: context.fatsRatio.getSnapshot().context.value,
            carbohydratesRatio: context.carbohydratesRatio.getSnapshot().context.value,
            proteinsRatio: context.proteinsRatio.getSnapshot().context.value,
          };
        },
      },
    },
  },
});

When the request fails (onError) or completes (onDone) we transition back to Idle:

You can use onError to implement a more robust solution, for example showing an error message to the user.

export const machine = setup({
  // ...
}).createMachine({
  // ...
  states: {
    Idle: {
      on: {
        "plan.update": {
          target: "Updating",
        },
        "plan.remove": {
          target: "Removing",
        },
        "plan.set": {
          target: "Setting",
        },
      },
    },
    Updating: {
      invoke: {
        src: "updatePlan",
        input: ({ event, context }) => {
          assertEvent(event, "plan.update");
          return {
            id: event.id,
            calories: context.calories.getSnapshot().context.value,
            fatsRatio: context.fatsRatio.getSnapshot().context.value,
            carbohydratesRatio: context.carbohydratesRatio.getSnapshot().context.value,
            proteinsRatio: context.proteinsRatio.getSnapshot().context.value,
          };
        },
        onError: { target: "Idle" },
        onDone: { target: "Idle" },
      },
    },
  },
});

With this the machine is complete. It handles all the actions related to a meal plan.

export const machine = setup({
  types: {
    input: {} as {
      calories: number;
      fatsRatio: number;
      carbohydratesRatio: number;
      proteinsRatio: number;
    },
    context: {} as {
      calories: ActorRefFrom<typeof numberFieldActor>;
      fatsRatio: ActorRefFrom<typeof numberFieldActor>;
      carbohydratesRatio: ActorRefFrom<typeof numberFieldActor>;
      proteinsRatio: ActorRefFrom<typeof numberFieldActor>;
    },
    events: {} as
      | { type: "plan.update"; id: number }
      | { type: "plan.remove"; id: number }
      | { type: "plan.set"; id: number },
  },
  actors: {
    updatePlan: fromPromise(
      ({
        input,
      }: {
        input: {
          id: number;
          calories: number;
          fatsRatio: number;
          carbohydratesRatio: number;
          proteinsRatio: number;
        };
      }) =>
        RuntimeClient.runPromise(
          Effect.gen(function* () {
            const api = yield* WriteApi;
            yield* Effect.log(input);
            yield* api.updatePlan(input);
          }).pipe(Effect.tapErrorCause(Effect.logError))
        )
    ),
    removePlan: fromPromise(({ input }: { input: { id: number } }) =>
      RuntimeClient.runPromise(
        Effect.gen(function* () {
          const api = yield* WriteApi;
          yield* Effect.log(input);
          yield* api.removePlan(input);
        }).pipe(Effect.tapErrorCause(Effect.logError))
      )
    ),
    setAsCurrentPlan: fromPromise(({ input }: { input: { id: number } }) =>
      RuntimeClient.runPromise(
        Effect.gen(function* () {
          const api = yield* WriteApi;
          yield* Effect.log(input);
          yield* api.updateCurrentPlan(input.id);
        }).pipe(Effect.tapErrorCause(Effect.logError))
      )
    ),
  },
}).createMachine({
  id: "manage-serving",
  context: ({ spawn, input }) => ({
    calories: spawn(numberFieldActor, {
      input: { initialValue: input.calories },
    }),
    carbohydratesRatio: spawn(numberFieldActor, {
      input: { initialValue: input.carbohydratesRatio },
    }),
    fatsRatio: spawn(numberFieldActor, {
      input: { initialValue: input.fatsRatio },
    }),
    proteinsRatio: spawn(numberFieldActor, {
      input: { initialValue: input.proteinsRatio },
    }),
  }),
  initial: "Idle",
  states: {
    Idle: {
      on: {
        "plan.update": {
          target: "Updating",
        },
        "plan.remove": {
          target: "Removing",
        },
        "plan.set": {
          target: "Setting",
        },
      },
    },
    Updating: {
      invoke: {
        src: "updatePlan",
        input: ({ event, context }) => {
          assertEvent(event, "plan.update");
          return {
            id: event.id,
            calories: context.calories.getSnapshot().context.value,
            fatsRatio: context.fatsRatio.getSnapshot().context.value,
            carbohydratesRatio:
              context.carbohydratesRatio.getSnapshot().context.value,
            proteinsRatio: context.proteinsRatio.getSnapshot().context.value,
          };
        },
        onError: { target: "Idle" },
        onDone: { target: "Idle" },
      },
    },
    Removing: {
      invoke: {
        src: "removePlan",
        input: ({ event }) => {
          assertEvent(event, "plan.remove");
          return { id: event.id };
        },
        onError: { target: "Idle" },
        onDone: { target: "Idle" },
      },
    },
    Setting: {
      invoke: {
        src: "setAsCurrentPlan",
        input: ({ event }) => {
          assertEvent(event, "plan.set");
          return { id: event.id };
        },
        onError: { target: "Idle" },
        onDone: { target: "Idle" },
      },
    },
  },
});