Browse documentation
Agent docs/query/optimistic-rollback
recipe

Implement a rollback-safe optimistic update

Cancel conflicting refetches, snapshot cache, write an immutable draft, roll back on failure, and revalidate on settlement.

View raw Markdown
CONTRACT

Execution contract

Use when
Use when mutation latency is noticeable, immediate feedback matters, and old cache can be snapshotted and restored exactly.
Avoid when
Show pending variables or wait for the response when complex permissions, server ordering, or multi-list rules cannot be reproduced accurately.
Preconditions
List all affected query keys, snapshot types, conflict responses, rollback policy, and final revalidation scope.
Verification
Cover success, network failure, validation error, conflict, and concurrent submission; every path converges on server state without a stale refetch overwriting the draft.
Failure mode
A flash back to old data usually means onMutate did not await cancelQueries; a draft surviving failure indicates a missing snapshot, in-place mutation, or mismatched rollback key.
Security
Optimistic UI is not proof of persistence or authorization; permission denial and domain conflicts must roll back, and sensitive operations should not appear completed prematurely.
01

Decide whether cache mutation is necessary

For one view, render mutation.variables while isPending; this is the lower-risk default. Use onMutate only when multiple cache consumers must update immediately. List exact query keys, old-value types, server failure semantics, and final revalidation scope.

02

Transactional implementation

onMutate must await cancelQueries before snapshotting and writing a new object. onError restores only when a snapshot exists. Return the invalidateQueries promise from onSettled so pending covers the full reconciliation window. Never mutate old cache in place.

src/features/issues/mutations.tsts
import { mutationOptions } from '@tanstack/react-query'

const issueKey = (id: string) => ['issues', 'detail', id] as const

export const renameIssueOptions = (issueId: string) => mutationOptions({
  mutationFn: (title: string) => renameIssue({ id: issueId, title }),
  onMutate: async (title, context) => {
    const key = issueKey(issueId)
    await context.client.cancelQueries({ queryKey: key })
    const previous = context.client.getQueryData<Issue>(key)
    context.client.setQueryData<Issue>(key, (issue) =>
      issue ? { ...issue, title } : issue,
    )
    return { previous }
  },
  onError: (_error, _title, result, context) => {
    if (result) context.client.setQueryData(issueKey(issueId), result.previous)
  },
  onSettled: (_data, _error, _title, _result, context) =>
    context.client.invalidateQueries({ queryKey: issueKey(issueId) }),
})
03

Concurrency and authorization

One mutationKey may expose several pending variables, so use submittedAt to identify temporary entries. Only the server response proves persistence and authorization; conflicts, validation failures, and permission denials must roll back or refetch rather than leaving optimistic cache as truth.

PRIMARY SOURCEShttps://tanstack.com/query/v5/docs/framework/react/guides/optimistic-updateshttps://tanstack.com/query/v5/docs/framework/react/guides/mutations
TanStack Atlas

Original bilingual knowledge · verified against primary sources

Friend linksGitHub