---
id: query.optimistic-rollback
kind: recipe
product: query
framework: react
locale: en
revision: 1
sourceCheckedOn: "2026-08-03"
versionRange: "^5"
verifiedAgainst: "@tanstack/react-query@5.101.4"
contentModel: 2
packages:
  - "@tanstack/react-query"
tasks:
  - "optimistic-update"
  - "rollback-mutation"
  - "reconcile-server-state"
sourceRefs:
  - "https://tanstack.com/query/v5/docs/framework/react/guides/optimistic-updates"
  - "https://tanstack.com/query/v5/docs/framework/react/guides/mutations"
---

# Implement a rollback-safe optimistic update

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

## 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.

## 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.

## 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.

File: `src/features/issues/mutations.ts`

```ts
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) }),
})
```

## 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.

## Official sources

- https://tanstack.com/query/v5/docs/framework/react/guides/optimistic-updates
- https://tanstack.com/query/v5/docs/framework/react/guides/mutations
