Optimistic UI
Updating the interface before the server has agreed: a claim the client makes on the authority's behalf, acceptable exactly when the rollback is honest.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
When is it right to show the user that something happened before the server has confirmed that it did?
Someone toggles a star, sends a message, reorders a list. They expect the interface to respond to their hand rather than to a round trip, and they expect what it shows them to be true.
Update local state immediately, fire the request, and if it comes back with an error show a toast that says something went wrong.
The toast is missed, auto-dismisses, or is shown on a screen the user has already navigated away from — and the interface still displays the optimistic value, which is now simply a false statement that persists until something refetches.
- The toast is missed, auto-dismisses, or is shown on a screen the user has already navigated away from — and the interface still displays the optimistic value, which is now simply a false statement that persists until something refetches.
- The failure path is almost never exercised in development, where the server is milliseconds away and always says yes. The first real execution of that code is in production, in front of a user.
- The server accepts but returns something different: a real id in place of your temporary one, a trimmed string, a server timestamp, a recomputed total, a name with "(2)" appended. Neither keeping your prediction nor reverting it is correct (Rollback and Reconciliation).
- A background revalidation that was already in flight lands between the optimistic write and the confirmation, overwriting the prediction with the server's pre-mutation value. The star un-stars itself for a second and then re-stars, and nobody can reproduce it (Stale-While-Revalidate).
- Optimistically rendering an outcome the server may refuse on authorization grounds teaches the user they can do something they cannot, and then takes it back (Authorization-Aware UI).
What is actually happening
In the browser, not in the framework.
- The full sequence is seven steps, not two: cancel in-flight queries for the affected keys, snapshot the current entries, write the predicted value, send the mutation, replace the prediction with the server's answer on success, restore the snapshot on failure, and mark the entries stale so the next read verifies.
- Cancelling first is not optional. A revalidation that is already on the wire carries pre-mutation data. If it resolves after your optimistic write, it silently reverts it, and the symptom is a flicker that appears only on slow connections (Cancelling a Request Nobody Is Waiting For).
- The prediction must be written as an updater over the cache entry, not as component state. Anything else forks the data: the component shows the prediction and every other view of the same key shows the old value (Who Owns This State?).
- The prediction is a guess about a computation you cannot perform. You do not have the other rows, the server clock, the uniqueness constraints or the authorization rules — which is exactly why the set of predictable mutations is small and identifiable.
- Retry requires idempotency. A mutation that may be sent twice needs the server to collapse the duplicate, which is a contract concern the client cannot solve alone (Idempotency Keys: The Mechanism).
- None of this changes what the server does. It changes only what the interface asserts during the interval, and who is accountable when that assertion turns out to be wrong.
What this makes the browser do
And which of it is avoidable.
- At least two renders per mutation — the prediction and the confirmation — and three when it fails, since the rollback is a third write. Each one is reconciliation plus whatever DOM, style, layout and paint the changed region needs (The Cost of a Change).
- A predicted insert into a long list re-renders the list twice: once with a temporary id, once with the real one. If the row is keyed by id, the second write destroys and recreates the node rather than updating it (Reconciliation and Keys).
- Predicting a change that affects sort order costs layout for everything below the moved row, twice — once on prediction and once on confirmation, if the server sorts it differently (Layout Thrashing).
- The optimistic write itself is synchronous main-thread work inside the event handler, so a prediction that maps over ten thousand rows makes the interaction it was meant to accelerate slower (Interaction Responsiveness).
The claim and the authority
It helps to state the pattern in terms of who is saying what. The user acts. The client makes a claim about what the server will decide. The server then decides. Between the claim and the decision there is an interval in which the interface is asserting something on the authority's behalf, without the authority's agreement.
That framing settles most of the design questions on its own. It is acceptable to speak for the authority when you are almost always right, when being wrong is cheap, and when you will correct yourself visibly and immediately. It is not acceptable when the authority is the only party that can know the answer — which is precisely the case for permissions, uniqueness, quota and money.
It also explains the cancellation step, which otherwise looks like a detail. A revalidation in flight is a *previous* answer from the authority, on its way. If it arrives after your claim, it overwrites your claim with older truth. You are not racing the server; you are racing an answer to a question you asked before you spoke.
When the prediction is allowed
The decision is not "is this mutation fast" or "would it feel nicer". It is two questions: can the client actually compute the outcome, and what does the user lose if the claim turns out to be false. Both have to be answered before the pattern is appropriate, and the second one is the one teams skip.
The options below are ordered from the most to the least optimistic. They are all legitimate; the point is that they are different products for the user, not different implementations of one product.
For this mutation, what is the honest thing to show during the round trip?
when The client can compute the result exactly, the server almost always accepts, and reverting is cheap and comprehensible: toggles, favourites, mark-as-read, local reordering, adding an item to a list the user owns.
cost Three code paths (predict, reconcile, roll back), a cancellation step, an announcement on both success and rollback, and a small rate of visible corrections that you must monitor.
when The prediction is reliable but the item is visibly new — a sent message, a created row. The item appears immediately, marked as sending.
cost A second visual state and a second announcement, plus a decision about what a pending item does when the user tries to act on it before it is confirmed.
when The outcome is not fully predictable, or the action reads as final: publish, submit, pay, delete, invite. The control shows the action in progress; the data does not change until the server says so.
cost The interaction feels as slow as the network is. On a high-latency connection that is a real cost, and it is the honest one.
when You want the responsiveness but the wording would be a claim: show the new state, but describe it as in progress ("Publishing…") rather than complete.
cost Requires writing two vocabularies for one state and keeping them in sync as the feature changes.
when The user is offline or the connection is unreliable, so the window is unbounded and there is no round trip to hide.
cost A durable queue, conflict handling on replay, a visible pending list, and an answer to what happens when a queued mutation fails hours later (The Offline Mutation Queue).
when The server is the only party that can know: uniqueness, quota, availability, price, permission, anything with a workflow or an approval.
cost Nothing but perceived speed — and buys an interface that never has to retract a statement (Authorization-Aware UI).
What the two frames cost, and the code that produces them
An optimistic mutation renders at least twice, and the second render is the one people forget to think about. If the predicted value and the server's value differ in a way that changes geometry — a real id that re-keys a row, a name the server normalised, a sort position that moved — the confirmation is not a free repaint. It is a second layout of the region, arriving after the user has already started reading the first.
The code below is framework-free on purpose: it is the seven steps as plain functions, so the order is visible. Every caching library implements some version of this, and the differences between them are mostly about which steps they do for you and which they hand back.
| Change | style | layout | paint | composite | Why |
|---|---|---|---|---|---|
| Toggle a boolean on one row (star, read, pinned) | yes | no | yes | yes | A class or attribute change on one element. Geometry is unchanged, so the confirmation frame is usually a no-op if the server agrees. |
| Optimistically insert a row with a temporary id | yes | yes | yes | yes | The list grows, so everything below reflows. Then the real id arrives and the row is re-keyed — a second insert and remove unless identity is preserved. |
| Reorder a list by drag | yes | yes | maybe | maybe | Reordering DOM nodes relayouts the container. A transform-based reorder can stay on the compositor, but only if positions are not also being written (Cheap and Expensive Animation). |
| Optimistic edit to a text field the server may normalise | yes | maybe | yes | yes | Layout depends on whether the normalised text is a different length — which you cannot know, which is exactly the problem. |
| Rollback of any of the above | yes | maybe | yes | yes | A third render. Cheap in browser terms and expensive in user terms: it is the frame where the interface contradicts itself. |
| Prediction that recomputes a derived total or count | yes | maybe | yes | yes | The derived value renders elsewhere on the page, so one optimistic write can invalidate regions the user is not looking at (Derived State). |
caveat Every row assumes the changed element is not on its own compositor layer and that the list is not virtualised. Virtualisation confines layout to the visible window, which changes the layout answers from yes to "yes, for the rows on screen" (List Virtualization).
1async function mutateOptimistically<T>(2 cache: Cache,3 key: string,4 predict: (previous: T | undefined) => T,5 send: () => Promise<T>,6): Promise<T> {7 // 1. A revalidation already on the wire carries PRE-mutation data.8 // If it lands after step 3, it silently reverts the prediction.9 await cache.cancelInFlight(key)10 11 // 2. Snapshot before writing. This is the only copy of the truth you had.12 const snapshot = cache.read<T>(key)13 14 // 3. The claim.15 cache.write(key, predict(snapshot))16 announce(politeDescriptionOf(key)) // the visual flip announces nothing17 18 try {19 // 4. Ask the only party that can actually decide.20 const authoritative = await send()21 22 // 5. Write the SERVER value — not your prediction, even if they look equal.23 // Ids, timestamps, normalised strings and computed fields live here.24 cache.write(key, authoritative)25 return authoritative26 } catch (err) {27 // 6. Rollback, and say so. A silent revert is the interface correcting a28 // lie in a way only sighted users who happened to be looking will see.29 cache.write(key, snapshot)30 announceAssertively(describeFailure(err))31 throw err32 } finally {33 // 7. You predicted once. Mark it so the next read verifies rather than34 // trusting a value that was, at one point, a guess.35 cache.markStale(key)36 }37}Step 5 is the one most implementations get wrong: on success they keep the prediction because it "worked". The server's response is authoritative for every field it contains, and the difference between the two is not an error — it is information the user may need (Rollback and Reconciliation).
How to build it
Most important first.
- Use it where the prediction is nearly always right and being wrong is small and reversible: toggles, favourites, marking read, reordering, adding an item to a list you own.
- Do not use it where the server is the only thing that can know the answer: uniqueness, quota, payment, availability, permission, anything computed server-side, anything that enters a workflow (What the Frontend Is Responsible For in Auth).
- Cancel in-flight queries for the affected keys, then snapshot, then write. In that order, every time.
- Render the action as in progress rather than the outcome as complete. "Publishing…" is honest; "Published" before the server said so is not — and for anything irreversible-sounding it is the difference between a good pattern and a lie.
- Make failure local, loud and recoverable: put the message next to the control that failed, keep the user's input, and offer the retry there. A corner toast is not an error affordance for a mutation the user has already moved past.
- Reconcile with the server's response rather than keeping the prediction on success, so the difference between predicted and actual is corrected while the user is still looking at it (Rollback and Reconciliation).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- An optimistic change needs the same announcement a confirmed one would get. A toggle that flips visually announces nothing, so a screen-reader user gets no feedback at all for the interaction they just performed.
- Do not move focus on the optimistic write, on the confirmation, or on the rollback. The user acted once; focus should move at most once, and usually not at all (Focus Management).
- A rollback is one of the rare cases where an assertive announcement is right: the interface stated something untrue and must correct it before the user acts on it. Put the message adjacent to the control, not in a corner (Live Regions and Announcement).
- Keep the control focusable and in place while the mutation is in flight. Use
aria-disabledandaria-busyrather than removing ordisabled-ing it — a removed element that had focus drops focus tobody, and adisabledcontrol cannot be reached to hear its own error (The Rules of ARIA). - Preserve node identity across the temporary-id-to-real-id swap, or the row the user just created is destroyed and rebuilt, taking their focus with it (Node Identity Across Updates, Reconciliation and Keys).
What can go wrong
- The rollback restores a snapshot that is no longer current, because a revalidation or a second mutation changed the entry in between. The revert undoes work that had nothing to do with the failure.
- Two optimistic updates on the same entity overlap; the first fails and its rollback wipes the second one's prediction along with its own.
- The mutation succeeded and the response was lost — a timeout, a dropped connection, a backgrounded tab. The client rolls back a change the server actually made, and the two disagree until the next fetch (Network Failures Only the Client Can See).
- The optimistic value is kept on success, so a server-normalised field (a trimmed name, a rounded amount, a canonical id) stays wrong in the cache indefinitely.
- The mitigation failing: rollback is implemented correctly and is completely silent. The value reverts, nothing is announced, and for a non-visual user the interface simply lied and then quietly corrected itself (Live Regions and Announcement).
- A background revalidation started before the mutation resolves after the optimistic write and reverts it. Cancelling queries for the affected keys before writing is the defence, and forgetting it is the single most common bug in this pattern (Cancelling a Request Nobody Is Waiting For).
- Two optimistic updates to the same entity are in flight at once; their responses arrive in the opposite order, and the entry ends up holding the older server value (Out-of-Order Responses).
- The first of two overlapping mutations fails, and its rollback restores a snapshot taken before the second one's prediction, erasing a change the user made and that may still succeed.
- An optimistic insert and a refetch that already contains the real row both land, producing the item twice — once under a temporary id and once under the real one (Reconciliation and Keys).
- The user navigates away between prediction and response, so the confirmation, the rollback and the announcement all target a screen that no longer exists (Route Loading Boundaries).
- An optimistic update is a client-side belief. It is not evidence that the action was permitted, performed, or persisted, and no other client-side decision may be derived from it (What the Frontend Is Responsible For in Auth).
- Never predict the outcome of an authorization check. Optimistically unlocking a section, revealing a field, or enabling an admin control renders a state the server may refuse — and the user, reasonably, believes what the interface showed them (Authorization-Aware UI).
- A retried non-idempotent mutation duplicates its effect. The client cannot make a
POSTsafe by itself; it needs an idempotency key the server honours (Idempotency Keys: The Mechanism). - Rollback must not be used to hide a server rejection the user needs to see. A refused payment, a validation failure, a permission denial: reverting the UI and saying nothing turns an actionable error into a mystery.
- The optimistic value sits in the same heap as everything else, so it is readable by any script on the origin — including one injected through an XSS, before the server has any record of it (Cross-Site Scripting).
- "Optimistic UI is a performance optimisation." It changes no timing at all. It changes what is displayed during a wait, which is a communication decision that happens to feel like speed (Interaction Responsiveness).
- "If it fails we roll back, so it is safe." Safe requires that the rollback is correct, is visible, and restores something that is still current. All three are work, and the third is not always possible.
- "The server accepted it, so the prediction was right." Accepted is not identical. The server's response is the authority for every field it contains, including the ones you guessed correctly (Rollback and Reconciliation).
- "We can optimistically show the action succeeded and reconcile later." For anything the user reads as final — published, refunded, deleted, sent — that is a claim you may have to retract, and users do not experience a retraction as a race condition. They experience it as the product being wrong.
Measuring it, and what changes in the field
- The single most useful metric here is a rollback rate per mutation type. A prediction is a hypothesis about the server, and a rising rollback rate is that hypothesis failing — long before anyone files a bug (Frontend Error Tracking).
- In the Network panel, the mutation's status and duration tell you the size of the window in which the interface was asserting something unconfirmed (Debugging the Network).
- Interaction responsiveness is what optimistic UI buys, so it is what should improve. If it does not, the prediction is doing too much work inside the event handler (Interaction Responsiveness).
- Field data on mutation failures by class — validation, conflict, authorization, transport — tells you which predictions to remove entirely (Real User Monitoring).
- On a slow network the optimistic window is long, which is when the pattern is most valuable and most dangerous: more time for a revalidation to land on top of it, and more time for the user to act on an unconfirmed state.
- Offline, the window is unbounded. That is a different pattern: a durable queue with visible pending state, not an optimistic write that will resolve shortly (The Offline Mutation Queue).
- On a slow device the optimistic render is not free, and a prediction that rebuilds a large list can cost more than the round trip it was hiding (List Virtualization).
- In a long-lived tab, unconfirmed mutations can accumulate across a network interruption, and their rollbacks then arrive together (Long-Lived Clients and Version Skew).
- Optimistic UI buys an interface that responds at the speed of the user's hand, and costs you three code paths instead of one, each with its own tests, its own announcements and its own races.
- Predicting more mutations makes more of the app feel instant and increases the number of places where the interface can be confidently wrong.
- The honest alternative — a pending state on the control while the request is in flight — costs perceived speed and buys an interface that never asserts anything it has not been told. For destructive or irreversible actions that trade is usually correct.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThe claim-and-authority framing, the seven-step sequence and the accessibility obligations hold regardless of implementation. Only the API for expressing them changes.
- FRAMEWORK-SPECIFICTanStack Query expresses this as
onMutate(cancel, snapshot, write),onError(restore from the returned context) andonSettled(invalidate); SWR does it withmutate(key, optimisticData, { rollbackOnError, revalidate }); Apollo Client and Relay take anoptimisticResponsethat is written to the normalised store and automatically discarded when the real response arrives, so rollback is implicit and snapshotting is not something you write. Advice that names lifecycle callbacks does not transfer to the last two. - NETWORK-SPECIFICThe size of the optimistic window is set entirely by round-trip time, so the pattern's value and its risk both scale with the network. On a fast local connection the failure paths are effectively untested; on a high-latency mobile link they are the common case.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Distributed Systems — an optimistic update is a local write to a replica whose authority is elsewhere, so it inherits the whole vocabulary of tentative writes: confirmation, conflict, reconciliation and the interval where two copies disagree.