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
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Managing state
  2. Mutations

Mutations#

mutation is query's counterpart for writes: same shape, triggered explicitly, owning its own loading and failure state.

Use it when you send something to a server — POST, PUT, PATCH, DELETE. Not when you read (query) or run an async action that isn't a server write (asyncProcess).

The common case#

typescript
import { CraftHttpClient, mutation } from '@craft-ts/core';

const { createUser } =
  yield *
  mutation('createUser', {
    method: (payload: { name: string; email: string }) => payload,
    loader: function* ({ params: user }) {
      return yield* CraftHttpClient.post(({ response }) => ({
        url: '/api/users',
        body: user,
        success: response<User>(),
      }));
    },
  });

// In a tracked generator, consume the trigger with yield*.
yield * createUser.mutate({ name: 'John', email: 'john@example.com' });

createUser.isLoading();
createUser.value(); // never throws
createUser.exception();

method is the entry point: it takes what the caller passes and returns what the loader receives as params. It is also where you reject bad input before any request happens.

TIP

value() is safe to read in templates and computed signals: it returns undefined when the mutation has no resolved value.

Connecting it to the read side#

A mutation on its own leaves your list stale. Declare the link on the query rather than reloading by hand:

typescript
insertReactOnMutation(createUser, { reload: { onMutationSuccess: true } });

That, plus optimistic updates, is on Reacting to mutations.

Triggering from an event#

Use a source$ as the trigger instead of calling .mutate(...):

typescript
const deleteUserSource = source$<{ name: string; email: string; id: string }>();

const { deleteUser } =
  yield *
  mutation('deleteUser', {
    method: on$(deleteUserSource, (payload) => payload),
    loader: function* ({ params: user }) {
      return yield* CraftHttpClient.delete(({ response }) => ({
        url: '/api/users',
        body: user,
        success: response<User>(),
      }));
    },
  });

deleteUserSource.emit({ name: 'John', email: 'john@example.com', id: '5' });

Rejecting bad input, and reading exceptions#

exceptions() is split by origin — params for what method rejected before any request, loader for what the request produced — and typed from the codes you declared:

typescript
const { deleteUser } =
  yield *
  mutation('deleteUser', {
    method: (payload: { userId: string }) =>
      payload.userId.length < 18
        ? craftException(
            { _tag: 'INVALID_ID' },
            { min: 18, received: payload.userId.length },
          )
        : payload.userId,
    loader: function* ({ params }) {
      return yield* CraftHttpClient.delete(({ response }) => ({
        url: '/api/user',
        body: params,
        success: response<User>(),
        exceptions: [
          function* ({ status }) {
            if (!(yield* status(403))) return;
            return craftException(
              { _tag: 'USER_ACCESS_FORBIDDEN' },
              { payload: params },
            );
          },
        ],
      }));
    },
  });

yield * deleteUser.mutate({ userId: 'ab' });
deleteUser.hasException(); // true
deleteUser.exceptions().params?.INVALID_ID;

yield * deleteUser.mutate({ userId: '12345-12344_27365453-2625434357282827' });
deleteUser.exceptions().loader?.USER_ACCESS_FORBIDDEN;

Returning a craftException from method means the loader never runs.

Pitfalls#

One in-flight run replaces the previous one unless you declare an identifier (below). Deleting three rows at once without one gives you the state of the last delete only.

No value is available yet. Check hasValue() or handle the undefined result while the mutation is loading or in exception.

Advanced — parallel mutations by identifier

identifier keeps one resource per key, so each row tracks its own state:

typescript
const { deleteUser } =
  yield *
  mutation('deleteUser', {
    method: (payload: { name: string; email: string; id: string }) => payload,
    identifier: ({ id }) => id,
    loader: function* ({ params: user }) {
      return yield* CraftHttpClient.delete(({ response }) => ({
        url: '/api/users',
        body: user,
        success: response<User>(),
      }));
    },
  });

yield * deleteUser.mutate({ name: 'John', email: 'john@example.com', id: '5' });

deleteUser.select('5')?.isLoading();
deleteUser.select('5')?.exception();
deleteUser.select('5')?.value();
Advanced — yielding dependencies

method, loader and the insertion can all be generators, and providers scopes dependencies to this mutation alone. A loader must not be async or return a native Promise; use yield* for asynchronous Craft operations:

typescript
const { saveUser } =
  yield *
  mutation('saveUser', {
    providers: [provideMutationLogger(), provideUserApiService()],
    method: function* (user: { id: string; name: string }) {
      yield* MutationLogger.log(`mutate:${user.id}`);
      return user;
    },
    loader: function* ({ params }) {
      return yield* UserApiService.save(params);
    },
  });

Advanced — injectable writes

Insertion methods provide injectMutationMethodRuntimeContext(), and the mutation value itself is published to providePrimitiveResourceRuntimeObserver. Both expose get, set, update, and patch for wrappers, WebMCP tools, and other advanced patterns. See Anatomy of a primitive.

See Also#

  • query — the read side

  • Reacting to mutations

  • Submitting a form — wiring a form to a mutation

←queryqueryParams→

On this page

  • The common case
  • Connecting it to the read side
  • Triggering from an event
  • Rejecting bad input, and reading exceptions
  • Pitfalls
  • See Also

@craft-ts · MIT licensed

GitHubnpm

Search