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.
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.
Turn this lesson into a verifiable skill
The optimistic value is visible while pending, failure restores the exact old value, and completion reconciles cache with server truth.
Optimistically rename an issue, inject one 409 failure, and submit two concurrent changes to observe rollback and concurrent pending state.
Test success, network failure, domain conflict, and concurrent submission; every path converges on server state and mutation pending lasts through revalidation.
- 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.