🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Optimistic Updates: Instant UI, Confirmed Later

Master optimistic updates with TanStack Query: onMutate, snapshot-based rollback, cancelQueries, and onSettled reconciliation.

Total XP: 0|💻 react XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Optimistic update fundamentals.

Quick Quiz //

What is the goal of an optimistic update?


🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

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

ChromeSupported

Fully supported.

FirefoxSupported

Fully supported.

SafariSupported

Fully supported.

EdgeSupported

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

THE BUG

A liked post occasionally reverts to unliked a moment after the like button was clicked, even though the request succeeded.

THE FIX

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.

THE BUG

After a failed mutation, the UI keeps showing the optimistic (incorrect) result indefinitely.

THE FIX

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] });
  },
});

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Implementing an optimistic update with no rollback logic in onError

onMutate: async (id) => { const previous = queryClient.getQueryData(['item', id]); queryClient.setQueryData(['item', id], predictedValue); return { previous }; }, onError: (err, id, context) => { queryClient.setQueryData(['item', id], context.previous); },

The Solution //

Without a rollback, a failed mutation leaves the cache permanently showing the incorrect optimistic guess. Always snapshot the prior state in onMutate and restore it in onError.

The Error //

An optimistic update flickers back to the old value moments after being applied

onMutate: async (id) => { await queryClient.cancelQueries({ queryKey: ['item', id] }); // now safe to write the optimistic value },

The Solution //

A background refetch already in progress resolved after the optimistic write and overwrote it. Cancel in-flight queries for the affected key at the start of onMutate before writing the optimistic value.

Lesson Glossary

[01]Optimistic Update

Showing the predicted result of a mutation immediately, before the server confirms it.

Code Preview
onMutate: () => setQueryData(...)

[02]onMutate

A useMutation callback firing before the request, used to write the optimistic value into the cache.

Code Preview
onMutate: async (vars) => {...}

[03]Rollback

Restoring the cache to its pre-optimistic state after a mutation fails.

Code Preview
onError: (err, vars, ctx) => setQueryData(key, ctx.previous)

[04]cancelQueries

A QueryClient method that cancels in-flight refetches, preventing a race with an optimistic write.

Code Preview
await queryClient.cancelQueries({ queryKey })

[05]onSettled

A useMutation callback that runs after either success or failure, used for final reconciliation.

Code Preview
onSettled: () => invalidateQueries(...)

Continue Learning