Articles

Optimistic UI Updates Explained

Optimistic UI updates the interface immediately, before the server confirms a change, then rolls back if the request fails — for apps that feel instant.

Takina Takina · · 4 min read
A laptop screen showing JavaScript code in a dark editor

Optimistic UI is a pattern where an interface updates immediately in response to a user action — before the server has confirmed the change succeeded — then quietly reconciles or rolls back if the request actually fails. Instead of showing a spinner and waiting on a round trip, the app assumes success and shows the end state right away, because in the overwhelming majority of cases, the request does succeed.

A “like” button that fills in the instant you tap it, a chat message that appears in the thread before the server has acknowledged it, a todo item that gets crossed off the moment you click it — all of these are optimistic updates. The alternative, waiting for a server response before changing anything, is called pessimistic UI, and it’s the safer but noticeably slower-feeling default.

Why wait when you can assume

Network round trips are the dominant source of perceived latency in interactive apps — often tens to hundreds of milliseconds even on a fast connection. That gap is imperceptible when nothing on screen has to wait for it, and glaringly obvious when a button visibly does nothing until a request resolves. Optimistic UI doesn’t make the network faster; it just removes the network from the user’s critical path for actions that are very likely to succeed. It’s a perceived-performance technique in the same family as good Core Web Vitals practices — both are about closing the gap between “the app did something” and “the user sees it.”

How it works

A typical optimistic update follows the same four-step shape regardless of framework:

  1. Snapshot the current state. Before mutating anything, keep a copy of what the UI looked like, so there’s something to restore if the request fails.
  2. Apply the update locally, immediately. Flip the UI to the new state as if the server had already agreed.
  3. Fire the request asynchronously. The actual network call — typically an async function awaiting a promise — runs in the background while the UI has already moved on.
  4. Reconcile on response. On success, replace any locally-guessed values (a temporary client-generated ID, a placeholder timestamp) with the server’s authoritative response. On failure, restore the snapshot from step 1 and surface an error.
async function toggleLike(postId) {
  const snapshot = getPostState(postId);
  setPostState(postId, { liked: true, likeCount: snapshot.likeCount + 1 });

  try {
    const result = await api.likePost(postId);
    setPostState(postId, result);
  } catch {
    setPostState(postId, snapshot);
    showError("Couldn't like this post — try again.");
  }
}

Handling failure gracefully

A silent rollback is confusing — the user saw their action succeed, then watched it quietly reverse with no explanation. A failed optimistic update should always surface something: a toast, an inline error, an undo affordance. It’s also worth designing the retry itself to be safe: if a flaky connection causes the client to resend a request, the server should treat a duplicate the same as the original rather than double-applying it, which is exactly the idempotency guarantee worth building into any mutation endpoint an optimistic UI depends on.

Where it breaks down

Optimistic UI isn’t the right default everywhere:

  • Irreversible or high-stakes actions. Charging a card, deleting an account, submitting a non-editable order — these should wait for confirmation, because a visible rollback after the fact is a worse experience than a short wait up front.
  • Outcomes the client can’t predict. If the server applies business logic the client doesn’t have — a discount code that might be invalid, a quota that might be exceeded — the optimistic guess can be wrong often enough that showing it isn’t actually helpful.
  • Overlapping updates racing each other. If a user fires several optimistic mutations against the same piece of state in quick succession (double-clicking a counter, editing the same field on two tabs), responses can land out of order and stomp on each other. This is a textbound race condition, and it needs the same discipline — sequencing, request cancellation, or last-write-wins with a version check — that any concurrent-write problem needs.

Optimistic UI in frameworks

Most modern data-fetching libraries formalize this pattern rather than leaving you to hand-roll the snapshot/rollback logic: React’s useOptimistic hook, and mutation helpers in libraries like React Query and SWR, all provide a declarative way to say “show this state immediately, then reconcile with the real response.” Fine-grained reactive systems like signals make the reconciliation step cheap, since only the specific piece of state that changed needs to re-render rather than a larger component tree. For updates that need to reflect changes from other clients too — not just the one that triggered the mutation — pairing optimistic local updates with a WebSocket push once the server confirms keeps everyone’s view consistent without every client polling for the answer.

The takeaway

Optimistic UI trades a small amount of correctness risk for a large improvement in perceived speed: update the interface immediately, keep a snapshot, and roll back with a visible message if the request fails. It’s the right default for low-stakes, high-success-rate actions like likes, toggles, and reorders, and the wrong one for anything irreversible or dependent on server-side logic the client can’t predict.

Takina Takina · · 5 min read

Promise.all() vs allSettled() vs race() Compared

Promise.all() fails fast, allSettled() waits for every result, and race() returns whichever promise finishes first — how to choose correctly.

#JavaScript #Web Development #Frontend
Takina Takina · · 4 min read

JavaScript Spread vs Rest Operators, Explained

The spread operator (...) expands an iterable into individual elements; the rest operator collects elements back into an array. Same syntax, opposite jobs.

#JavaScript #Web Development #Frontend
Takina Takina · · 4 min read

ResizeObserver API Explained: Watching Element Size

The ResizeObserver API lets JavaScript watch an element's box size and react without polling or resize-event hacks. How it works and when to use it.

#Web Development #JavaScript #Frontend