Capítulo 26 de 80

Chapter 26: Mutations

Core Idea

useMutation wraps a create/update/delete side-effect with the same status-tracking model as queries, plus a callback lifecycle (onMutate/onError/onSuccess/onSettled) that runs whether triggered via mutate or mutateAsync.

Key Concepts

  • States: isIdle/isPending/isError/isSuccess (mirrors query status, plus an explicit idle/reset state); error and data populate accordingly.
  • Triggering: mutate(variables) fires-and-forgets with callbacks; mutateAsync(variables) returns a Promise you can await/try/catch directly for composing side effects.
  • reset(): clears a mutation's error/data back to idle — needed since, unlike queries, a mutation's result isn't automatically cleared between calls.
  • Callback lifecycle & signature: onMutate(variables) fires before the request (return a value to use for rollback later); onError(error, variables, onMutateResult); onSuccess(data, variables, onMutateResult); onSettled(data, error, variables, onMutateResult) — always last, success or failure. Returning a Promise from a callback makes the next callback wait for it.
  • Hook-level vs call-level callbacks: callbacks passed to useMutation itself always fire; the same callback names can also be passed to mutate(vars, { onSuccess, ... }) for one-off, call-site-specific side effects — hook-level callbacks fire first, then call-level ones, and call-level callbacks are skipped if the component unmounts before the mutation finishes.
  • Consecutive mutations difference: useMutation's own callbacks run for every mutate() call; callbacks passed directly to mutate() fire only once, for the last call, even if several mutate() calls are in flight — because each mutate() call resubscribes the mutation observer.
  • Retry: mutations do not retry by default (unlike queries' default of 3) — opt in explicitly with retry. Mutations that fail due to being offline retry in original call order once reconnected.
  • Scopes: mutations run in parallel by default even for repeated calls of the same mutation; give matching mutations a scope: { id } to force them to run serially — later ones queue with isPaused: true until earlier ones in the same scope finish.
  • Persisting paused mutations: queryClient.setMutationDefaults(mutationKey, options) registers a mutation's config globally so it survives dehydration; combined with dehydrate/hydrate/resumePausedMutations(), offline-paused mutations can resume after an app restart — but only if a default mutationFn was registered, since functions can't be serialized to storage.
  • Event pooling gotcha (React ≤16): don't pass mutate directly as an event handler if the mutation function reads the event — wrap it in a plain handler that calls event.preventDefault() synchronously first.

Code Examples

const mutation = useMutation({ mutationFn: addTodo })

// Fire-and-forget with callbacks
mutation.mutate({ title: 'Do Laundry' })

// Promise-based, for composing with other async logic
try {
  const todo = await mutation.mutateAsync({ title: 'Do Laundry' })
} catch (error) {
  // handle
}
  • What it demonstrates: the two ways to trigger the same mutation — callback-driven mutate vs. awaitable mutateAsync.

Key Takeaways

  1. Use mutateAsync when you need to await the result to drive further logic; use mutate for the common fire-and-forget-with-callbacks case.
  2. Remember mutations don't retry by default — add retry explicitly if transient failures should be tolerated.
  3. Give related mutations a shared scope.id when they must not race each other (e.g. sequential edits to the same resource).

Connects To

  • Query Invalidation / Updates from Mutation Responses: what onSuccess typically does.
  • Optimistic Updates: builds directly on onMutate's rollback-context pattern.
  • persistQueryClient: the plugin referenced for persisting paused offline mutations.