Browse documentation
Lesson 17 · Optimistic writes

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.

TanStack QueryProduction25 minREV 01Markdown .md
After this lesson

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

01

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.

02

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.

src/features/issues/useRenameIssue.tstsx
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 }),
  })
}
03

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

Turn this lesson into a verifiable skill

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.
SOURCE REFERENCES · CHECKED 2026-08-03https://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