skip to content

A React UI shows a 'like' as applied immediately and sends the request in the background. What must that optimistic update keep hold of to be safe, and what has to happen when the request fails?

level: seniorimportance: should knowfreq 42%

answer

  1. you are rendering a prediction
  2. restore a snapshot, not an inverse
  3. several may be in flight at once
  4. the response is the truth, not your guess
  5. a silent revert looks like a bug

basics

~20 s

An optimistic update must retain the pre-update value so it can be reverted, and must treat the server's response as the truth once it arrives. On failure it reverts to that snapshot and tells the user the action did not happen — a silent revert reads as the app losing their input.

solid answer

~50 s

Showing the change before it is confirmed means you are rendering a prediction, so you need three things. First, the value as it was before, because rollback is restoring a snapshot rather than applying an inverse operation — inverses go wrong as soon as a second update lands in between. Second, an identity for the in-flight operation, so a late failure reverts the right thing when several are outstanding. Third, a rule that the server's response wins: it may normalize fields, assign a real id in place of your temporary one, or reject the action outright. On failure you revert *and* surface it — a like that silently un-likes itself looks like a bug in your app, not a rejected request. In React 19, `useOptimistic` gives you this shape: it returns the optimistic value while an action is pending and drops it when the action settles, so the underlying state must have been updated or revalidated by then.

code

javascript · 14 lines
javascript
// Rollback by snapshot, not by inverse operation
async function toggleLike(cache, key, liked) {
  const previous = cache.get(key);           // 1. snapshot the confirmed value
  const opId = Symbol('toggleLike');         // 2. identity for this operation
  cache.set(key, { ...previous, liked, likes: previous.likes + (liked ? 1 : -1) });
  try {
    const confirmed = await api.setLike(key, liked);
    cache.set(key, confirmed);               // 3. server response is the truth
  } catch (error) {
    cache.set(key, previous);                // restore, do not decrement
    notifyFailure(opId, error);              // failure must be visible
    throw error;
  }
}

go deeper

for a junior

Know what an optimistic update is — showing the result before the server confirms — and that the code must be able to put the old value back if the request fails.

for a middle

Explain the mechanics: snapshot before applying, reconcile with the response, restore the snapshot on failure, and why an inverse operation is not a safe undo when other updates can interleave.

for a senior

Demonstrate the production concerns: several operations in flight at once, a revalidation landing mid-prediction, temporary ids being replaced, and failure that must be surfaced next to the thing the user acted on rather than swallowed.

for a principal

Own the policy for which action classes may be predicted at all, what the standard failure and retry experience is, and how prediction interacts with the caching layer so every team is not inventing its own rollback semantics.

## What optimistic actually means An optimistic update renders the *predicted* result of a request before the server has confirmed it. It exists because a like button that waits 400ms to fill in feels broken, and it is a trade: you buy perceived latency with a small chance of showing something that turns out to be false. That trade is only sound if you have planned for the false case. The failure mode of a careless optimistic update is not "a request failed" — it is "the UI is now lying and nothing will correct it". ## The three things you must hold **The previous value.** Rollback means restoring the snapshot you took before applying the prediction, not applying an inverse. Inverses look tempting — increment, so decrement on failure — and they are wrong the moment anything else touched the value in between: a background refresh, a second like from another tab, or a concurrent update to a different field of the same entity. Snapshot and restore is exact; arithmetic undo is a guess. **An identity for the operation.** If the user likes three posts quickly, three requests are in flight. A failure that arrives second must revert *its* change and leave the other two alone. That requires each pending operation to carry an id and to be tracked, not a single boolean `isSaving`. **The knowledge that the server's answer is the truth.** Your prediction is an approximation. The server may trim or normalize a string, compute a derived field, assign a real id where you invented a temporary one, or apply business rules you did not model. When the response arrives, reconcile: replace the predicted entity with what came back, and fix up any references keyed by the temporary id. If you leave the prediction in place because it "looks right", you have quietly forked from the server. ## Failure is a UI event, not just a state rollback Reverting silently is the most common mistake. From the user's point of view the heart filled in and then emptied itself for no reason; nobody attributes that to a 500. Failure handling needs three parts: restore the snapshot, tell the user in a way tied to the thing they acted on, and offer the next step — retry, or leave it reverted. For destructive or expensive actions, offer an undo window instead of an optimistic apply, so the risky operation only starts when the window closes. ## Where optimistic updates are the wrong call Not everything should be predicted. If the outcome depends on server-side state you cannot see — inventory, permissions, a balance, a rate limit — the probability of being wrong is high enough that predicting it is dishonest. Payments and irreversible operations are the clearest case: never show "paid" before it is. The rule of thumb is that optimistic updates are for actions that essentially always succeed and whose failure is cheap to correct. They also interact badly with revalidation. A background refresh that lands while a prediction is pending can wipe the prediction out, making the UI flicker between the predicted and the server value. The cache entry must know that a prediction is layered on top of the confirmed value, rather than storing the prediction *as* the confirmed value. ## What React 19 gives you `useOptimistic` implements exactly this layering. You pass it the real state and a function that applies a predicted change; it returns the optimistic value while an action is pending and automatically discards it once the action completes, falling back to whatever the real state is by then. The important consequences of that design: - It must be driven from an action or a `startTransition`, because "pending" means an in-flight transition. - Because the optimistic layer is discarded on settle, the underlying state has to have been updated or revalidated by then — otherwise the UI snaps back to the old value on success, which looks identical to a failed request. - Rollback on failure is free precisely because the prediction was never written into the real state: dropping the layer *is* the rollback. Related action primitives — `useActionState` for the result and pending status of a form action, `useFormStatus` for a nested submit control — carry the surrounding status, but the prediction layer itself is `useOptimistic`. ## How to answer it out loud Say that you are rendering a prediction; that rollback is snapshot restoration, not an inverse operation; that concurrent operations need identities; that the server response reconciles the prediction; that failure must be visible; and that some actions should never be predicted at all. That sequence covers everything an interviewer is listening for.

  • Why is restoring a snapshot better than applying the inverse operation on failure?
    Because an inverse assumes nothing else changed the value in between. If a background refresh landed, another tab acted, or a second mutation touched the same entity, decrementing a count or flipping a flag produces a value that matches neither the prediction nor the server. Restoring the exact pre-update snapshot is unambiguous, and reconciling with the server's response afterwards makes it correct.
  • What goes wrong if a background revalidation lands while an optimistic update is still pending?
    If the prediction was written into the cached value, the refresh overwrites it and the UI flickers back to the old state before the request even completes. The fix is layering: keep the confirmed value and the prediction separate so a refresh updates the base while the prediction still renders on top. That separation is exactly what `useOptimistic` provides.
  • Which actions would you refuse to make optimistic?
    Anything whose success depends on server state you cannot see — inventory, permissions, balances, quotas — and anything irreversible or financially meaningful. Showing "paid", "booked" or "deleted" before confirmation risks a correction the user cannot distinguish from a bug. For destructive actions an undo window is better: it feels instant and the request only fires when the window closes.
  • When a create returns a server-assigned id, what has to happen to the temporary one?
    Every reference to the placeholder must be rewritten when the response lands: the cache key, the list entry, any selection or scroll anchor pointing at it, and any follow-up request the user has already triggered against it. Getting this wrong produces a row that reappears duplicated, or an action that 404s because it targeted an id the server never had.

saying these in an interview costs you the question

  • Rolls back by applying an inverse operation instead of restoring
  • Reverts silently and never tells the user it failed
  • Tracks in-flight work with one boolean instead of per-operation ids
  • Keeps the predicted value instead of the server's response
  • Applies optimistic updates to payments or irreversible actions

context