Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources

i.Learn with Effect

  • Effect users: start here
  • Overview
  • 1. Start with a Craft component
  • 2. Derive UI state
  • 3. Put the domain in Effect
  • 4. Load data with Effect
  • 5. Write data with Effect
  • 6. Provide Layers and route the app
  • 7. Build forms and validate boundaries
  • 8. Test the graph
  • 9. Server functions — POC
  1. Learn with Effect
  2. 5. Write data with Effect

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:

typescript
  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:

typescript
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:

typescript
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:

typescript
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:

typescript
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.

← 4. Load data with Effect

6. Provide Layers and route the app →

←4. Load data with Effect6. Provide Layers and route the app→

On this page

  • mutationEffect
  • asyncProcessEffect
  • Validate arguments before the Effect
  • What you gained

@craft-ts · MIT licensed

GitHubnpm

Search