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.
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.