--- sidebar_position: 4 title: Mutation --- # Mutation The Mutation module is Effect View's counterpart to TanStack Query mutations. It models user-triggered asynchronous work such as saving a profile, deleting a record, uploading a file, or sending a command to an API. The familiar concepts are present, but the operation itself is an Effect: | TanStack Mutation concept | Effect View equivalent | | --- | --- | | Mutation variables | The input key `K` | | `mutationFn` | `f: (key: K) => Effect` | | Mutation result | `mutation.state`, a `View>` | | `isPending` | `result.waiting` | | `mutateAsync` | `mutation.mutate(key)` | | Start without awaiting | `mutation.mutateView(key)` | Unlike a Query, a Mutation has no cache, reactive key, or automatic execution. It runs only when `mutate` or `mutateView` is called. This makes it suitable for commands, while Query remains the model for cached server state. ## Create a mutation Create a mutation in a component scope with `Mutation.make`. Its function can be any Effect, including one that requires services from the application runtime: `Mutation.make` is an Effect constructor, not a hook. Create each mutation instance once and keep it stable instead of calling `Mutation.make` again on every render. Use `Component.useOnMount` for a component-owned mutation or create it in an Effect service when it should be shared. ```tsx import { Effect } from "effect" import { AsyncResult } from "effect/unstable/reactivity" import { Component, Mutation, View } from "effect-view" import { sendInvite } from "./api" interface InviteInput { readonly email: string readonly role: "member" | "admin" } const InviteButtonView = Component.make("InviteButton")( function* (props: { readonly email: string }) { const mutation = yield* Component.useOnMount(() => Mutation.make({ f: (input: InviteInput) => sendInvite(input), }), ) const [result] = yield* View.useAll([mutation.state]) const runSync = yield* Component.useRunSync() return (
{AsyncResult.match(result, { onInitial: () => null, onFailure: ({ cause }) => (

Could not send invite: {cause.toString()}

), onSuccess: ({ value }) => (

Invite sent to {value.email}

), })}
) }, ) ``` `Mutation.make` captures the current Effect context. If `sendInvite` requires an HTTP client, authentication service, tracer, or another service, provide it to the runtime or component layer before creating the mutation. The click handler does not need to reconstruct those dependencies. The mutation also belongs to the creation scope. Any mutation fibers still running when that scope closes are interrupted automatically. ## Understand AsyncResult `mutation.state` starts as `Initial` with `waiting: false`. Starting a mutation changes it to a waiting state, then publishes either `Success` or `Failure`: ```tsx const [result] = yield* View.useAll([mutation.state]) return AsyncResult.match(result, { onInitial: ({ waiting }) => waiting ?

Starting...

:

Ready.

, onFailure: ({ cause, previousSuccess, waiting }) => (

{cause.toString()}

{previousSuccess._tag === "Some" && (

Last saved value: {previousSuccess.value.value.name}

)} {waiting &&

Trying again...

}
), onSuccess: ({ value, waiting }) => (

Saved {value.name}

{waiting &&

Saving a newer value...

}
), }) ``` The `waiting` flag is independent from the result tag. After one successful call, starting another keeps the successful value and sets `waiting: true`. If that next call fails, the failure can retain the earlier success in `previousSuccess`. Components can therefore keep useful feedback visible during retries or repeated submissions. A failure contains an Effect `Cause`, not only `E`. Typed failures, defects, and interruption information remain available for logging or presentation. ## Choose mutate or mutateView Both methods start the same mutation effect. They differ in what the caller waits for: | Method | Return value | Best suited to | | --- | --- | --- | | `mutate(key)` | The final `Success` or `Failure` | Effect workflows that need the outcome. | | `mutateView(key)` | A live `View>` | UI callbacks that should return after starting the work. | Use `mutate` with an asynchronous component runner when later logic depends on the final result: ```tsx const runPromise = yield* Component.useRunPromise() const save = () => void runPromise( Effect.gen(function* () { const result = yield* mutation.mutate(input) if (AsyncResult.isSuccess(result)) { yield* Effect.log(`Saved record ${result.value.id}`) } }), ) ``` The mutation Effect does not fail with `E`. It captures the operation's `Exit` and returns a final `AsyncResult.Success` or `AsyncResult.Failure`. Inspect or match that value when control flow depends on the outcome. Use `mutateView` when the UI only needs to start the operation and react to its state: ```tsx const runSync = yield* Component.useRunSync() const startSave = () => { const state = runSync(mutation.mutateView(input)) // `state` is a View for this specific call. } ``` `mutateView` starts the scoped work and returns immediately with a per-call View. The shared `mutation.state` is also updated as that call progresses. ## Track the latest call In addition to `state`, a mutation exposes reactive metadata: | Member | Meaning | | --- | --- | | `state` | The latest mutation state published to the shared View. | | `latestKey` | The most recently supplied input as an `Option`. | | `latestFinalResult` | The latest completed success or failure as an `Option`. | | `fiber` | The most recently started mutation fiber as an `Option`. | Subscribe to any of them with `View.useAll`: ```tsx const [result, latestInput, latestFinal] = yield* View.useAll([ mutation.state, mutation.latestKey, mutation.latestFinalResult, ]) ``` The input is called a `key` in the generic API, but it is equivalent to mutation variables in TanStack Query. It can be a primitive, tuple, struct, or any other value accepted by the mutation function. ## Concurrent mutations Starting a mutation does not automatically interrupt an earlier mutation. Calls may overlap, and each call has its own state View. This is useful for independent operations such as uploading several files. When calls overlap, `mutation.state` reflects updates published by all calls; the last update to arrive wins. Use the View returned by `mutateView` when each concurrent operation needs its own progress indicator: ```tsx const upload = (file: File) => Effect.gen(function* () { const uploadState = yield* mutation.mutateView(file) // Store or pass `uploadState` to the row rendering this file. return uploadState }) ``` For a single submit button, disabling it while `mutation.state.waiting` is usually enough to prevent accidental overlap. For “latest request wins” behavior, model that policy explicitly or use Query when the operation is actually a reactive read. ## Update queries after a mutation Mutations do not invalidate Query caches automatically. Compose invalidation with the mutation result so the relationship remains explicit and typed: ```tsx const saveAndRefresh = (input: UpdatePostInput) => Effect.gen(function* () { const result = yield* updatePost.mutate(input) if (AsyncResult.isSuccess(result)) { yield* posts.invalidateCacheEntry(["post", result.value.id] as const) yield* posts.refreshView } return result }) ``` This is the Effect equivalent of an `onSuccess` callback that invalidates a TanStack Query. Because it is ordinary Effect composition, the workflow can also include tracing, transactions, retries, notifications, or parallel cache updates without introducing a separate callback API. Remember that Query invalidation removes cached data but does not refetch by itself. Follow it with `refreshView` when the current screen should update immediately. ## The Effect touch Mutation follows the ergonomics of TanStack mutations while retaining Effect's execution model: - The mutation function has the full `Effect` type. - Required services are captured from the creation context. - Failures are represented as `Cause`, including defects and interruption. - Fibers are scoped, so component unmounting cleans up in-flight operations. - `mutate` composes directly inside larger Effect workflows. - `mutateView` exposes call-specific progress as a View for React or Effect consumers. - Retry schedules, timeouts, tracing, logging, metrics, schema validation, and concurrency controls can be applied with normal Effect operators. Mutation is therefore a small bridge: TanStack-style mutation state on one side, and an ordinary, typed Effect program on the other.