# Mutations

> The four ways to read a mutation, MutationResult, mutate and mutateAsync, per-call callbacks, identity, and mutation defaults.

A mutation is a write. It has the same four call styles as a query —
`context.mutation(...)`, `watchMutation(...)`, `MutationBuilder`,
`MutationController` — and every one of them hands back a **controller**,
because you need `mutate` as well as the state.

```dart
final add = context.mutation(
  MutationOptions.simple(
    mutationFn: api.addTask,
    onSuccess: (_, __, ___) => client.invalidateQueries(
      filters: QueryFilters(queryKey: tasksKey),
    ),
  ),
);

// … add.value is the MutationResult; add.mutate(vars) starts it.
```

`MutationResult` is sealed: `MutationIdle`, `MutationPending`,
`MutationSuccess`, `MutationError`, with `isIdle` / `isPending` / `isSuccess` /
`isError` for the cases where a `switch` is more than you need.

## Three type arguments, and why `simple` exists

`MutationOptions<TData, TVariables, TOnMutateResult>`. The third is what
`onMutate` returns — the **rollback handle** of an optimistic update.

A mutation without an optimistic step has nothing to roll back, so
`MutationOptions.simple` fixes that third argument to `void` and lets the other
two infer from `mutationFn`. That is the only reason it exists: a typedef
cannot fix one type argument of a constructor and leave the rest to inference.

## `mutate` versus `mutateAsync`

- `mutate(vars)` starts it and returns nothing. Errors go to `onError` and to
  the result; nothing is thrown at the call site.
- `mutateAsync(vars)` returns a `Future<TData>` that **rejects** on failure.
  Use it when the caller genuinely wants to `await` the outcome — and then
  handle the rejection, or an unhandled async error will find you.

Per-call callbacks ride along, and run *after* the options' own:

```dart
add.mutate(
  'New task',
  callbacks: MutateCallbacks<void, String, void>(
    onSuccess: (data, vars, _) => Navigator.of(context).pop(),
  ),
);
```

A mutation does not retry unless you ask: its `retry` defaults to
`RetryPolicy.never`, because repeating a write is rarely safe. Pass a policy
when the endpoint is idempotent.

The showcase's `mutations` screen walks through all of this on one counter.
Press *Increment (mutate)* and *Increment (mutateAsync)* and watch the status
go `pending`, then `success`; tick *Fail next* for an error after exactly one
request; *Run with callbacks* logs the callbacks in the order they run.

Live demo: [Mutations](https://dualmeta-gmbh.github.io/query_kit/demo/showcase/#/mutations), running in the browser against an in-memory backend ([source](https://github.com/dualmeta-gmbh/query_kit/tree/main/examples/showcase/lib/features/mutations)). mutate, mutateAsync, reset, callbacks, and scopes.

## In an app

A mutation's options belong next to the queries it affects — a
`lib/data/device_mutations.dart` beside `device_queries.dart` — as a function
of what it needs. Here, the power switch of one device in a smart-home app:
the server answers with the device as it now is, which goes straight into
the cache, and the room lists refresh behind it.

```dart
// lib/data/device_mutations.dart
MutationOptions<Device, bool, void> setPowerMutation(
  QueryClient client,
  String id,
) =>
    MutationOptions.simple(
      mutationFn: (bool on) => devices.setPower(id, on: on),
      onSuccess: (device, _, __) {
        client.setQueryData<Device>(DeviceKeys.detail(id), device);
        client
            .invalidateQueries(
              filters: QueryFilters(queryKey: DeviceKeys.lists),
            )
            .ignore();
      },
    );
```

The switch reads the mutation in any of the four call styles. They hand you
the same controller — `value` for the state, `mutate` to start a run — and
behave the same; pick the one your widget already uses.

**context.mutation**

```dart
class PowerSwitch extends StatelessWidget {
  const PowerSwitch({super.key, required this.device});

  final Device device;

  @override
  Widget build(BuildContext context) {
    final client = QueryClientProvider.of(context);
    final power = context.mutation(setPowerMutation(client, device.id));
    final result = power.value;
    return Switch(
      // While the write is out, show what was asked for.
      value: result.isPending ? result.variables! : device.isOn,
      onChanged: result.isPending ? null : power.mutate,
    );
  }
}
```

**MutationBuilder**

```dart
Widget powerSwitch(QueryClient client, Device device) => MutationBuilder(
      options: setPowerMutation(client, device.id),
      builder: (context, power) {
        final result = power.value;
        return Switch(
          value: result.isPending ? result.variables! : device.isOn,
          onChanged: result.isPending ? null : power.mutate,
        );
      },
    );
```

**QueryMixin**

```dart
class _MixinPowerSwitchState extends State<MixinPowerSwitch> with QueryMixin {
  @override
  Widget build(BuildContext context) {
    final device = widget.device;
    final power = watchMutation(setPowerMutation(queryClient, device.id));
    final result = power.value;
    return Switch(
      value: result.isPending ? result.variables! : device.isOn,
      onChanged: result.isPending ? null : power.mutate,
    );
  }
}
```

**MutationController**

```dart
class _ControllerPowerSwitchState extends State<ControllerPowerSwitch> {
  late final QueryClient _client = QueryClientProvider.read(context);
  late final MutationController<Device, bool, void> _power =
      MutationController(_client, setPowerMutation(_client, widget.device.id));

  @override
  void dispose() {
    _power.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) =>
      ValueListenableBuilder<MutationResult<Device, bool>>(
        valueListenable: _power,
        builder: (context, result, _) => Switch(
          value: result.isPending ? result.variables! : widget.device.isOn,
          onChanged: result.isPending ? null : _power.mutate,
        ),
      );
}
```

While the write is out, the switch shows the value it was asked for — the
mutation's `variables` — and is disabled; if the write fails, it falls back to
what the cache says. That is an [optimistic update](https://dualmeta-gmbh.github.io/query_kit/docs/guides/optimistic-updates.md) in
its simplest form. In a list, give each row's widget a `ValueKey` of the
device's id, so a row's mutation stays with its device when the list
reorders.

## A mutation outlives its widget

Disposing a controller does not cancel the mutation — a write the user started
should normally finish; call `cancel()` first when it should not. That has one
practical consequence, and it bites everyone once:

```dart
@override
Widget build(BuildContext context) {
  // Take the client HERE, not inside onSuccess. The callback can run after
  // this element is gone, and looking an ancestor up from a deactivated
  // element throws.
  final client = QueryClientProvider.of(context);
  final add = context.mutation(MutationOptions.simple(
    mutationFn: api.addTask,
    onSuccess: (_, __, ___) => client.invalidateQueries(/* … */),
  ));
  // …
}
```

The cache work has to happen either way. The client is the right thing to close
over; the `BuildContext` is not.

Per-call callbacks — the `callbacks:` passed to `mutate` — are the exception:
once the reader has stopped listening, they are skipped, and only the
options' own callbacks run. Put what must happen in the options.

## Narrowing rebuilds

`context.mutation`, `watchMutation` and `MutationBuilder` take
[`buildWhen`](https://dualmeta-gmbh.github.io/query_kit/docs/guides/render-optimizations.md#buildwhen); a `MutationController`
has none, on purpose — it is the notifier. It is the only narrowing a
mutation reader has: there is no `select` on a mutation.

## Identity

In the context and mixin styles a mutation is identified by `id:` if you give
one, else by its `mutationKey`, each together with its three type arguments;
without either, by the types alone. A `mutationKey` is a category, as
TanStack Query's is, not a name: two mutations of the same shape under one key read
in one `build` without an `id:` would share one controller, and whichever was
read last would run for both — its function and its callbacks alike. When
their mutation functions or callbacks (`onMutate`, `onSuccess`, `onError`,
`onSettled`) differ, a debug build catches it with an assertion: "delete,
then pop" and "delete, then show a snackbar" are two mutations. So does a
different `scope`, `retry`, `retryDelay`, `networkMode` or `gcTime`: rows
reading `MutationScope('task-$id')` inline under one key would otherwise
share one queue. Give each an `id:`. Those five compare by value, except
the two that carry a closure — `RetryPolicy.when` and `RetryDelay.dynamic`
— which compare by variant only, so a helper building one inline and read
twice is still one mutation. `meta` is not compared — it is most often a map
literal, new on every build — and the last read's wins.

The same functions read twice are one mutation and do not assert: a getter
over one stored options object, options built around tear-offs or top-level
functions, or a nested builder re-reading what `build` read. A function
literal is a new function every time it is evaluated, so a getter that builds
one per read looks exactly like two mutations and still asserts — keep the
options (or the functions) in a field, or read the mutation once and share
the controller. Only a `StatelessWidget`'s or `State`'s own `build` is
checked; reads through a `LayoutBuilder`'s context never are.

Like a query, a mutation is released after the frame once a build stops reading
it.

## Defaults

`client.setMutationDefaults(key, MutationDefaults(...))` registers
`mutationFn`, `retry`, `retryDelay`, `networkMode`, `gcTime`, `scope` and
`meta` per key. **Not callbacks** — see [differences from TanStack
Query](https://dualmeta-gmbh.github.io/query_kit/docs/reference/differences-from-tanstack.md).

A mutation with neither a `mutationFn` nor a `mutationFnWithContext` anywhere
fails with
`MissingMutationFunctionError`, and is never retried.

> **Note: In React Query**
>
> This page is `useMutation`. `mutate` and `mutateAsync` behave as they do
> there, and so does the order of the callbacks. Two things differ: a mutation
> has four call styles here rather than one hook, and `setMutationDefaults`
> takes no callbacks. See [differences from TanStack
> Query](https://dualmeta-gmbh.github.io/query_kit/docs/reference/differences-from-tanstack.md).

## Where next

- [Invalidations from mutations](https://dualmeta-gmbh.github.io/query_kit/docs/guides/invalidations-from-mutations.md) and
  [updates from mutation responses](https://dualmeta-gmbh.github.io/query_kit/docs/guides/updates-from-mutation-responses.md) — making
  the cache agree with the write.
- [Optimistic updates](https://dualmeta-gmbh.github.io/query_kit/docs/guides/optimistic-updates.md) — showing the write before the
  server confirms it.
- [Mutation scopes](https://dualmeta-gmbh.github.io/query_kit/docs/guides/mutation-scopes.md), [cancelling
  mutations](https://dualmeta-gmbh.github.io/query_kit/docs/guides/cancelling-mutations.md) and [mutation state](https://dualmeta-gmbh.github.io/query_kit/docs/guides/mutation-state.md).
- Offline, a mutation is paused rather than failed; see [network
  mode](https://dualmeta-gmbh.github.io/query_kit/docs/guides/network-mode.md).
