Act on input, not on response

Update the UI on click, sync in the background, roll back on failure.

Guillermo Rauch7 Principles of Rich Web Applications
JavaScript allows us to mask network latency altogether. Applying this as a design principle should even remove most spinners or “loading” messages from your applications.
rauchg.com · 2014Read article ↗
Guillermo Rauch7 Principles of Rich Web Applications
A click on a link or button can result in an immediate reaction without hitting the network. A famous example of this is Gmail (or Google Inbox), where archiving an email will happen immediately on the UI while the server request is sent and processed asynchronously.
rauchg.com · 2014Read article ↗
Guillermo Rauch@rauchg
Some engineering principles I live by: … ✓ Feedback must be given to users instantly
Nov 3, 2024View on X ↗
Guillermo Rauch@rauchg
I first fell in love with React when I saw the incredible experiences they were shipping on FB Newsfeed. Realtime comments, notifications, chat, infinite scroll, optimistic likes… Always work backwards from the user experience when picking your tools.
Dec 1, 2023View on X ↗

Demo 1 rebuilds the Gmail archive from his essay; demo 3, the optimistic like.

1 · Archive now, sync later

Archive a few emails in each inbox.

Waits for the serverBad
Waited0ms
Acts on the clickGood
Waited0ms
const [emails, removeOptimistic] = useOptimistic(
  serverEmails,
  (list, id) => list.filter((e) => e.id !== id),
);

const archive = (id) =>
  startTransition(async () => {
    removeOptimistic(id);               // the row leaves now
    const res = await archiveEmail(id); // server action, runs behind
    if (!res.ok) toast.error("Couldn't archive", { action: retry(id) });
  }); // on failure serverEmails still has it: the row comes back

2 · Roll back when it fails

Keep “Server fails” on and archive an email in each pane.

No rollbackBad
Rolls back + retryGood
async function archive(email) {
  const index = list.indexOf(email);
  list.remove(email);                         // optimistic
  try {
    await api.archive(email.id);
    toast("Archived", { action: { label: "Undo", onClick: () => list.insert(index, email) } });
  } catch {
    list.insert(index, email);                // roll back to the truth
    toast.error("Couldn't archive", { action: { label: "Retry", onClick: () => archive(email) } });
  }
}

3 · The optimistic like

Like each post, then tap it again quickly.

Awaits the responseBad
Felt0ms
OptimisticGood
Felt0ms

Why it matters, pros and cons

Why it matters

Almost every request succeeds. Show that success now and handle the rare failure when it comes.

Pros

  • Zero perceived latency, on any network.
  • No spinner per row; the list stays usable.
  • Undo comes nearly free: it's the same rollback.

Cons

  • Payments, logout: acting early “can result in a negative experience.”
  • Needs a real rollback path and a visible error.
  • Server-made values (ids, totals) still arrive later.