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 aPromise
that resolves with the result of the request.
Each request is composed by:
input
tofromPromise
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" },
},
},
},
});