5. Write data with Effect#
Goal: model writes and explicit processes as Effects while retaining Craft's mutation lifecycle.
mutationEffect#
Use method for the synchronous argument-to-params mapping and loader for the Effect program:
const saveTask = yield* mutationEffect('saveTask', {
method: (input: { readonly title: string }) => input,
loader: ({ params }) => saveTaskEffect(params),
});
yield* saveTask.mutate({ title: 'Ship the Effect guide' });
The mutation exposes loading, value and typed exceptions().loader just like a native Craft mutation. Compose it with the list query using the normal Craft insertion:
const tasksQuery = yield* queryEffect(
'tasks',
{
params: () => ({ done: false }),
loader: ({ params }) => listTasksEffect(params),
},
insertReactOnMutation(saveTask, {
reload: { onMutationSuccess: true },
}),
);
Use an optimistic insertion when the result can be predicted locally:
const tasksQuery = yield* queryEffect(
'tasks',
{
params: () => ({ done: false }),
loader: ({ params }) => listTasksEffect(params),
},
insertReactOnMutation(saveTask, {
optimisticPatch: {
title: ({ mutationParams }) => mutationParams.title,
},
reload: { onMutationException: true },
}),
);
Effect remains responsible for the operation and its typed errors; Craft remains responsible for when the resource is refreshed and what the UI renders.
asyncProcessEffect#
Use asyncProcessEffect for an explicit process that is not a read cache or a write resource:
const refresh = yield* asyncProcessEffect('refresh', {
method: (userId: string) => userId,
loader: ({ params }) => refreshProfile(params),
});
yield* refresh.method('user-ada');
This is useful for refresh actions, exports, background commands and similar flows. Do not turn every Effect into an async process: choose queryEffect for server state, mutationEffect for writes and asyncProcessEffect for explicit commands.
Validate arguments before the Effect#
Effect Schema can be used anywhere Craft accepts a Standard Schema. Convert it once:
import { Schema } from 'effect';
const SaveTask = Schema.toStandardSchemaV1(
Schema.Struct({ title: Schema.String }),
);
const saveTask = yield* mutationEffect('saveTask', {
methodSchema: SaveTask,
method: (input) => input,
loader: ({ params }) => saveTaskEffect(params),
});
For a schema failure, Craft reports a parse exception. For a business rule such as “this title already exists”, return a tagged Effect error from the Effect program instead. See Effect Schema for synchronous and asynchronous decoding rules.
What you gained#
Typed Effect writes with Craft's loading, cancellation, cache invalidation and optimistic-update machinery. Next, provide the services that those programs require at app and route scope.