Waiting even 200ms for a server round-trip before updating the UI feels sluggish for frequent, low-risk actions like a like button. Optimistic updates show the predicted result immediately. This lesson covers onMutate, rollback on failure, preventing race conditions, and reconciling with onSettled.
1Waiting for the Server Feels Slow
Even a fast API call carries real latency, commonly 100-300ms. For frequent, low-risk interactions like liking a post, requiring the user to wait for a full round-trip before seeing any visual change feels sluggish. Optimistic updates instead show the expected result immediately, before server confirmation.
2The onMutate Callback
onMutate runs before mutationFn even sends its request, providing the exact moment to manually write the expected result into the query cache with queryClient.setQueryData, so the UI reflects the change the instant the user acts.
3Rolling Back on Failure
If the underlying request fails, the optimistic write must be undone, or the UI permanently shows something that never actually happened on the server. onMutate returns a snapshot of the prior data, which onError receives as context to restore the cache to its pre-optimistic state.
4Preventing Race Conditions with cancelQueries
If a background refetch is already in progress when an optimistic write happens, it could resolve afterward and overwrite the optimistic update with stale data. Calling queryClient.cancelQueries at the start of onMutate cancels any in-flight refetch for that key first, avoiding this race condition.
5Always Reconcile with onSettled
onSettled runs after the mutation completes, whether it succeeds or fails, and is the recommended place for a final invalidateQueries call — guaranteeing the cache eventually matches the true server state exactly, even if the original optimistic prediction was subtly incorrect.
6Step-by-Step Breakdown
Waiting for the Server Feels Slow. Even a fast API call has real latency — 100-300ms is common. For frequent, low-risk interactions like liking a post or checking off a todo, making the user wait for a round-trip before showing any change feels sluggish. Optimistic updates show the expected result immediately, before the server confirms it.
The onMutate Callback. onMutate runs BEFORE mutationFn even sends its request. This is where you manually update the query cache to reflect the expected result immediately, using queryClient.setQueryData, so the UI updates the instant the user acts.
When does onMutate run, relative to the actual network request in mutationFn?
- →Before — onMutate fires prior to the request being sent
- →After — once the server has already responded
Rolling Back on Failure. If the server request fails, the optimistic update needs to be undone — otherwise the UI permanently shows something that never actually happened. onMutate returns a snapshot of the previous data, and onError receives that snapshot as its third argument to roll the cache back.
Preventing Race Conditions with cancelQueries. If a background refetch is already in flight when onMutate fires, it could resolve right after your optimistic write and overwrite it with stale data — a race condition. Calling queryClient.cancelQueries at the start of onMutate cancels any in-flight refetch for that key first, avoiding this.
Why call queryClient.cancelQueries at the start of onMutate, before writing the optimistic value?
- →To prevent an in-flight background refetch from overwriting the optimistic update
- →It makes the mutation's own request execute faster
Always Reconcile with onSettled. Whether the mutation succeeds or fails, onSettled runs afterward — the right place to invalidate the query one final time, guaranteeing the cache eventually matches the real server state exactly, even if the optimistic guess was subtly wrong.
Mastery Achieved. You now understand the full optimistic update pattern: onMutate for the instant predicted write, snapshotting for rollback on onError, cancelQueries to prevent a race with an in-flight refetch, and onSettled for a final, guaranteed reconciliation with real server data. Next, you'll learn Infinite Queries for paginated and infinite-scroll data.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1A Rolled-Back Optimistic Update Needs a Clear Failure Announcement
If an optimistic like or edit silently reverts after a failed request, a user relying on assistive technology may not notice the change was undone — pair rollback with a clear error message (e.g. via a toast with role='alert').
SEO Implications
- 1
Optimistic Updates Have No Direct SEO Effect
This pattern governs client-side perceived interaction speed after a user action, with no bearing on server-rendered content or crawlability.
Best Practices
Reserve Optimistic Updates for High-Confidence, Low-Risk Actions
Actions that succeed the vast majority of the time (likes, toggles, simple field edits) are good optimistic-update candidates; actions prone to failure or with serious consequences (payments) are usually better served by an honest pending state.
Always Implement Rollback, Never Skip onError
An optimistic update without a rollback path leaves the UI permanently showing incorrect data after any failure — always snapshot the prior state in onMutate and restore it in onError.
Frequent Bugs
A liked post occasionally reverts to unliked a moment after the like button was clicked, even though the request succeeded.
A background refetch that was already in flight resolved after the optimistic write and overwrote it with pre-mutation data. Call queryClient.cancelQueries for the affected key at the start of onMutate to prevent this race.
After a failed mutation, the UI keeps showing the optimistic (incorrect) result indefinitely.
The onError handler isn't restoring the snapshot captured in onMutate. Return { previous: queryClient.getQueryData(key) } from onMutate and call queryClient.setQueryData(key, context.previous) inside onError.
Real-World Examples
An Optimistic Like Button
A social feed's like button needs to feel instant despite network latency. onMutate immediately increments the like count and marks the post as liked in the cache; if the request fails, onError restores the previous snapshot; onSettled always invalidates the query afterward to guarantee eventual consistency with the real server count.
useMutation({
mutationFn: (postId) => likePost(postId),
onMutate: async (postId) => {
await queryClient.cancelQueries({ queryKey: ['post', postId] });
const previous = queryClient.getQueryData(['post', postId]);
queryClient.setQueryData(['post', postId], (old) => ({ ...old, liked: true, likes: old.likes + 1 }));
return { previous };
},
onError: (err, postId, context) => {
queryClient.setQueryData(['post', postId], context.previous);
},
onSettled: (data, error, postId) => {
queryClient.invalidateQueries({ queryKey: ['post', postId] });
},
});