---
id: optimistic-updates
track: learn
product: query
locale: en
order: 17
revision: 1
sourceCheckedOn: "2026-08-03"
versionRange: "Query ^5"
verifiedAgainst: "@tanstack/react-query@5.101.4"
contentModel: 2
prerequisites:
  - "Mutation lifecycle and cache invalidation"
  - "A mutation endpoint that returns a stable entity or can be refetched"
sourceRefs:
  - "https://tanstack.com/query/v5/docs/framework/react/guides/optimistic-updates"
  - "https://tanstack.com/query/v5/docs/framework/react/guides/mutations"
---

# Make optimistic updates safe to roll back

> Choose UI-only pending output or a cache-level update, then handle failure and concurrency with cancellation, snapshots, rollback, and revalidation.

**Outcome:** Implement rollback on failure, reconcile with server truth on success, and explain how concurrent mutations are displayed.

## Choose the smallest optimistic scope

If pending output is only needed beside the mutation, render variables while isPending. This keeps cache untouched and needs no rollback. Update cache directly only when several views must see the change immediately; mutationKey and useMutationState can expose pending variables across components.

## Cache updates need a four-step transaction

In onMutate, cancel relevant refetches that could overwrite the optimistic value, snapshot old data, and write the draft immutably. Return the snapshot for onError rollback. Return the invalidateQueries promise from onSettled so the mutation remains pending until server truth re-enters the cache.

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

```tsx
import { useMutation } from '@tanstack/react-query'

export function useRenameIssue(issueId: string) {
  const key = ['issues', 'detail', issueId] as const
  return useMutation({
    mutationFn: (title: string) => renameIssue({ id: issueId, title }),
    onMutate: async (title, context) => {
      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(key, result.previous)
    },
    onSettled: (_data, _error, _title, _result, context) =>
      context.client.invalidateQueries({ queryKey: key }),
  })
}
```

## Do not assume only one mutation is pending

Several submissions can share one mutationKey, so useMutationState returns an array; submittedAt is a useful temporary identity. If list filters, sorting, or permission rules are hard to reproduce exactly on the client, narrow the optimistic scope or show pending items only, then reconcile from the server response and invalidation.

## Practice and verification

### Completion checkpoint

The optimistic value is visible while pending, failure restores the exact old value, and completion reconciles cache with server truth.

### Exercise

Optimistically rename an issue, inject one 409 failure, and submit two concurrent changes to observe rollback and concurrent pending state.

### Verification

Test success, network failure, domain conflict, and concurrent submission; every path converges on server state and mutation pending lasts through revalidation.

### Common pitfalls

- Not cancelling an in-flight refetch, allowing stale data to overwrite the optimistic value.
- Mutating cached objects in place and breaking subscriptions or rollback snapshots.

## Official sources

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