Capítulo 30 de 80

Chapter 30: Optimistic Updates

Core Idea

Two independent ways to show a mutation's result before the server confirms it: the simpler "via the UI" approach renders mutation.variables directly while isPending, needing no rollback logic; the "via the cache" approach writes the optimistic value into setQueryData from onMutate and rolls it back in onError — needed when more than one place on screen must reflect the pending change.

Key Concepts

  • Via the UI: read variables (and isPending/isError) straight off the mutation result and render an extra, visually-distinct (e.g. dimmed) item for the in-flight mutation; it disappears automatically once the invalidation-driven refetch (from onSettled) lands. On error, variables stays available (not cleared), so you can keep showing the failed item with a retry button.
  • Cross-component access: when the mutation and the list that should show it live in different components, useMutationState({ filters: { mutationKey, status: 'pending' }, select }) reads pending mutations' variables from anywhere, keyed by a shared mutationKey. The result is always an array (multiple concurrent mutations possible) — mutation.state.submittedAt makes a good unique key per item.
  • Via the cache: onMutate should (1) cancelQueries for the affected key so an in-flight background refetch can't stomp the optimistic write, (2) snapshot the current cache value with getQueryData, (3) write the optimistic value with setQueryData, and (4) return the snapshot so onError can restore it exactly (context.client.setQueryData(key, onMutateResult.previousTodos)). onSettled still invalidates afterward regardless of outcome, to reconcile with the server's real state.
  • When to use which: prefer "via the UI" when the optimistic result only needs to show in one place — it needs no rollback code at all. Reach for "via the cache" only when multiple UI locations must reflect the pending state, since the cache write propagates everywhere automatically.

Code Examples

// Via the UI — no rollback needed
const { isPending, variables, mutate } = useMutation({
  mutationFn: (newTodo) => axios.post('/api/data', { text: newTodo }),
  onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
// render: {isPending && <li style={{ opacity: 0.5 }}>{variables}</li>}

// Via the cache — full rollback pattern
useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo, context) => {
    await context.client.cancelQueries({ queryKey: ['todos'] })
    const previousTodos = context.client.getQueryData(['todos'])
    context.client.setQueryData(['todos'], (old) => [...old, newTodo])
    return { previousTodos }
  },
  onError: (err, newTodo, onMutateResult, context) => {
    context.client.setQueryData(['todos'], onMutateResult.previousTodos)
  },
  onSettled: (data, error, variables, onMutateResult, context) =>
    context.client.invalidateQueries({ queryKey: ['todos'] }),
})
  • What it demonstrates: the two optimistic-update strategies side by side — the UI-driven approach needing no rollback, and the cache-driven approach's full cancel → snapshot → write → rollback-on-error cycle.

Key Takeaways

  1. Default to "via the UI" (variables + isPending) — it's less code and needs no rollback logic, and covers the common single-location case.
  2. Reach for the cache-based approach specifically when several UI locations must reflect the same pending change simultaneously.
  3. Always cancelQueries before an optimistic setQueryData write in onMutate — otherwise a concurrent background refetch can silently overwrite your optimistic value before the mutation even resolves.

Connects To

  • useMutationState: powers the cross-component "via the UI" variant.
  • Query Cancellation: cancelQueries, required before the cache-based optimistic write.
  • Query Invalidation: onSettled's reconciliation step in both strategies.