Skip to the content.

Optimistic updates: show it now, reconcile later

Optimistic updates: show it now, reconcile later

An optimistic update shows the result of an action immediately, before the server has confirmed it — you assume success, update the UI now, and reconcile when the response arrives. Like a “❤” that fills the instant you tap it. It makes an app feel instant instead of laggy, because the user is not waiting on a round-trip to see their own action land. But the word “optimistic” is doing real work: you are betting the request will succeed, and a bet needs a plan for losing. The whole discipline is in the rollback — capturing enough state to undo the change cleanly when the server disagrees. Skip that and an optimistic UI is just a UI that lies.

Apply the change immediately, send the request, then confirm or roll back A user action updates the UI instantly and fires a request. On success the optimistic state is confirmed; on failure it rolls back to the snapshot taken before the change. user acts UI updates NOW+ snapshot taken request success → confirm fail → roll back restore snapshot
Update immediately and snapshot the old value; on success confirm, on failure restore the snapshot. The rollback is the part that makes it safe.

Snapshot, apply, then confirm or revert

The pattern has three moves and the first is the one people skip: capture the current value before you change it, so you have something to restore. Then apply optimistically, fire the request, and branch on the outcome:

async function toggleLike(id) {
  const previous = store.getState().posts.byId[id].liked;   // 1. snapshot
  dispatch({ type: "LIKE_TOGGLED", id });                    // 2. apply NOW (instant UI)
  try {
    await api.setLike(id, !previous);                        // 3a. server confirms → keep it
  } catch (err) {
    dispatch({ type: "LIKE_TOGGLED", id });                  // 3b. failed → revert to snapshot
    toast("Couldn't update — try again");
  }
}

For a toggle the revert is symmetric (toggle back); for an add/remove or an edit you restore the captured previous value explicitly.

The server’s response is the source of truth

Optimism is a guess about the server’s answer, so when the real answer arrives it wins. Often the server returns the authoritative record — a real id for the item you optimistically added, a server-computed field — and you reconcile your placeholder with it:

// added a comment with a temp id; replace it with the server's real record
const temp = { id: `temp-${Date.now()}`, text, pending: true };
dispatch({ type: "COMMENT_ADDED", comment: temp });
const saved = await api.postComment(text);
dispatch({ type: "COMMENT_CONFIRMED", tempId: temp.id, comment: saved });  // real id, no dupe

The pending flag lets you style the in-flight item subtly (dimmed, no delete button) so the user senses it is not yet final without being blocked.

Use it where success is likely and reversible

Optimistic updates fit actions that usually succeed and are cheap to undo — likes, toggles, reordering, adding a to-do, marking read. They are a poor fit for actions that frequently fail or whose reversal is confusing or impossible — a payment, an irreversible delete, anything with strong consistency needs — where the honest UX is a pending state and a confirmed result. The failure path must be visible: a silent rollback that flips the UI back with no explanation is worse than a spinner, because the user thinks their action worked. So always pair the revert with a message. Done well, optimistic UI is the single biggest perceived-speed win available for write actions; done without a rollback plan, it is a bug generator. The offline-first-list exercise builds exactly this snapshot-apply-reconcile loop, where optimism is not a luxury but the only way the UI can respond at all.