Skip to main content

Optimistic updates — advancing the UI without waiting for a response

The moment you tap "like", the heart turns red. Behind the scenes, the request to the server is still in flight. This technique — advancing the UI first, on the assumption of success — is called an optimistic update.

Used well, it changes the perceived speed dramatically. But get "what gets faster" wrong and you'll add all the complexity without the payoff.

What you'll learn in this chapter​

  • What optimistic updates are and aren't doing
  • The four implementation steps, and how to roll back on failure
  • Criteria for when to use it and when not to
  • Why server-side validation cannot be skipped
Prerequisites

This chapter builds on the "state as a single source of truth" idea from Declarative and imperative UI.

What this chapter does not cover​

Library-specific implementation details are covered by existing guides.

Not waiting, by telling a temporary lie​

Line up the normal flow and the optimistic update flow.

The only difference is the order of reflection. The request still fires once. Wait time isn't erased; it's hidden from the user.

If it fails, the temporary lie has to be undone.

This rollback is mandatory. Without it, you're left with a state where the screen says success but the server has no record — the most dangerous state to leave an optimistic update in.

The four implementation steps​

Whether you use a library or not, the work is the same.

  1. Cancel any in-flight refetches — a refetch running in the background can complete later and overwrite your optimistic value with an older one. Handlers run in completion order, and there's no guarantee they finish in start order (Event loop)
  2. Snapshot the current value — you need somewhere to roll back to
  3. Apply the optimistic value — the screen changes here
  4. Branch on success/failure — on failure, restore the snapshot. On either outcome, at the end, refetch from the server to align with the true value

TanStack Query maps these four onto three hooks.

useMutation({
mutationFn: toggleLike,
onMutate: async (newLike) => {
// 1. Cancel in-flight refetches
await queryClient.cancelQueries({queryKey: ['likes']});
// 2. Snapshot the current value
const previous = queryClient.getQueryData(['likes']);
// 3. Apply the optimistic value
queryClient.setQueryData(['likes'], newLike);
return {previous};
},
onError: (_err, _newLike, context) => {
// 4a. On failure, restore the snapshot
queryClient.setQueryData(['likes'], context?.previous);
},
onSettled: () => {
// 4b. Regardless of outcome, refetch to align with the true value
queryClient.invalidateQueries({queryKey: ['likes']});
},
});

React 19's useOptimistic removes the need to write the rollback.

const [optimisticCount, setOptimisticCount] = useOptimistic(count);

startTransition(async () => {
setOptimisticCount(count + 1); // Apply first
try {
const next = await like(); // Communicate
setCount(next); // On success, adopt the real value
} catch (e) {
showError(e); // On failure, don't update the real value
}
});

If you don't update the real state (count) on failure, the optimistic value vanishes when the transition ends and the display reverts. The design is that the rollback happens not by "writing the code to undo it" but by "not writing the code to advance it". Error notification is a separate concern, so catch still gets written.

What gets faster, and what doesn't​

Here's where the judgment tends to slip.

ItemDoes it change?
Perceived speedImproves (that's the point)
Number of requestsDoesn't change. Actually often increases due to confirmation refetches
Server processing timeDoesn't change
Server loadDoesn't change, or increases slightly
BandwidthDoesn't change, or increases slightly

Optimistic updates are not an optimization that reduces network calls. The request still fires; the server still processes it. What changes is only what the user sees while waiting for the result.

If you want to reduce the number of requests, that's the job of something else (debouncing, batching, caching).

When to use it and when not to​

The judgment comes down to three conditions.

Use it when these three line up:

  1. Success rate is high — normally succeeds; failure is exceptional
  2. The impact of failure is small — reverting and apologizing is enough
  3. The result is predictable client-side — you know what will happen without waiting on the server response

Likes, bookmarks, read/unread flags, reordering, toggling a checkbox — all fit.

Avoid it in cases like these:

Case to avoidReason
Payment, money transferThe psychological impact of failure is large. You must not make the user believe "it went through"
Reserving inventory, booking a seatIt can fail due to contention with other users. The high-success-rate assumption breaks
Operations whose value is decided by the serverNumbering, aggregation, server timestamps — client cannot predict
Operations that can't be undoneSent emails, external system integrations

"Can you predict the result" is easy to overlook. For example, if you optimistically handle "post a comment", the comment ID and timestamp are decided by the server, so the client can only place placeholders. If those placeholders are used as keys, you'll need to swap them out when the real values return.

Server-side validation cannot be skipped​

Optimistic updates are about display on the client — they are not a mechanism to guarantee data correctness. They are not a reason to skip server-side validation.

Two reasons.

  • The client's prediction is only a prediction. If someone else is updating at the same time, it can miss
  • Requests coming from the client can be tampered with. You cannot block direct requests that bypass your browser UI

The division of labor is clear. Client-side validation is for UX (giving instant feedback); server-side validation is for correctness (keeping bad data out). Adding optimistic updates doesn't move that division by even a millimeter.

A similarly named different concept — optimistic locking​

"Optimistic update" and "optimistic locking" have similar names but live at different layers and address different concerns.

Optimistic updateOptimistic locking
LayerClient UIDatabase concurrency control
PurposeImprove perceived speedPrevent lost updates from concurrent writes
What it doesUpdate the display without waiting for the responseKeep a version number; detect a mismatch at update time

What they share is only the mindset: "assume it usually works, then clean up if it doesn't." Implementing one does not give you the other for free. Optimistic locking is covered in Locks and isolation levels.

Common misconceptions​

"Optimistic updates reduce the number of server requests." — They don't. The request count stays the same. Adding confirmation refetches makes it larger. What improves is only perceived speed.

"Because the client optimistically maintains consistency, server-side validation can be skipped." — It can't. The client's prediction is not a guarantee, and requests can be tampered with. Server-side validation is mandatory.

"You can skip writing the rollback because refetching will fix it." — During the refetch, the screen is lying. The refetch itself can fail. The rule is to revert as soon as failure is detected.

"Optimistic updates and optimistic locking are the same thing." — Different concepts. Different layers, different purposes.

Exercises​

Q1. When you add an optimistic update, how many server requests per operation are there?

Answer and explanation

Answer: Not fewer. The update request stays at one; if you add a confirmation refetch, it becomes two.

What optimistic updates change is only what the user sees while waiting for the response. The number of network calls doesn't change, and the server-side work doesn't change either.

TanStack Query's standard style calls invalidateQueries in onSettled, so 1 update + 1 refetch means the count actually grows. It's still adopted because the value of improved perceived speed is judged to outweigh the extra network cost.

Q2. Which of the following is not suited to an optimistic update? Explain why.

A. Toggling a bookmark on an article
B. E-commerce "add to cart" (stock is 1 left)
C. Checking off a TODO as done
D. Marking a notification as read
Answer and explanation

Answer: B

Check against the three conditions.

High success rateSmall failure impactPredictable result
A BookmarkHighSmallYes
B Add to cart (1 left)Low (contention with other users)Large (making them believe they got it)Yes
C TODO doneHighSmallYes
D Mark as readHighSmallYes

B has inventory contention, so success can't be assumed. Showing "added to cart" and then reversing with "sorry, out of stock" is worse UX than making them wait a bit up front.

Q3. Why does the optimistic update implementation cancel in-flight requests at the start of onMutate?

Answer and explanation

Answer: Because an in-flight fetch can complete later and overwrite the optimistic value with the older one.

Suppose a list refetch is running in the background when the user taps "like".

  1. The refetch starts (server value at this point is "not liked")
  2. The user taps; the state is optimistically rewritten to "liked"
  3. The refetch from step 1 completes and overwrites the cache with "not liked"
  4. The screen reverts to "not liked"

From the user's perspective, they tapped and it reverted. To prevent that, in-flight fetches are canceled before the optimistic value is written.

Summary​

  • An optimistic update advances the UI on the assumption of success, without waiting for the server's response
  • You must roll the UI back to the previous state on failure. Skipping this leaves the screen and the server out of sync
  • The implementation is four steps: cancel in-flight refetches → snapshot current value → apply optimistic value → branch on outcome
  • Request count doesn't decrease. Only perceived speed improves; adding a refetch actually increases the count
  • Use for operations with high success rate, small failure impact, and predictable results. Not for payment or inventory reservation
  • Server-side validation cannot be skipped. Client validation is for UX; server validation is for correctness
  • It's only similar in name to optimistic locking — a different concept
Related references