Basics

What is optimistic UI — and why the like registers before the server answers

Illustration: the result is posted before the answer arrives

Here's a funny thing: when you tap Like, the heart turns red instantly. The server knows nothing about it yet. The request just left, the answer will land in 200–800 milliseconds — but you already see the result.

The interface lied to you. On purpose. It even has a name — optimistic UI.

What it is

Optimistic UI means the app shows the result of an action right away, without waiting for the server to confirm. It assumes the request will almost certainly succeed: the network is up, permissions are fine, the database is alive.

The normal (pessimistic) path:

  1. You tap Like.
  2. The button goes into a spinner.
  3. The server answers 200 OK.
  4. The heart turns red.

The optimistic path:

  1. You tap Like.
  2. The heart turns red.
  3. The request goes out in the background.
  4. If the server returns an error, the heart goes back.

To the eye, that's the difference between "this app is sluggish" and "this app is responsive" — even though network latency is identical in both. All that changed is what happens during those few hundred milliseconds.

Where it works, and where it doesn't

Optimism suits actions that are small, frequent and reversible: likes, checkboxes in a to-do list, favourites, sending a chat message, dragging a card.

Signs you can do it:

  • You know exactly what the result looks like before the server replies (red heart, done).
  • Failure is unlikely.
  • The rollback is harmless: putting the heart back is not a disaster.

Where you shouldn't:

  • Payments. You cannot show "Paid" before the bank answers.
  • Anything the server decides. If the backend assigns an order number, you can't guess it.
  • Anything that leads to the next step. If someone sees "Done" and navigates away, you'll never show them the rollback.

That last one is the real trap. Optimistic UI doesn't remove the risk — it moves it onto the user. While the server is silent, the person believes the job is finished and acts accordingly.

How to roll back properly

The work in optimistic UI isn't "show it instantly" — that's one line. The work is the rollback.

A bad rollback: the value silently reverts. At best the person assumes they missed the button and taps again. At worst they never notice the like didn't stick.

A good rollback does three things:

  • Restores the state — the heart is grey again.
  • Says what happened — "Couldn't save your like, you're offline."
  • Offers a Retry button — the decision was already made; don't make them make it twice.

A special case of the same mechanic is undo instead of "Are you sure?": the result shows immediately there too, and the safety net comes after.

If you're unsure where to put that message — a toast or a line next to the element — see toast or inline error: they have different jobs, and for rollbacks the rule isn't obvious.

What it looks like in code

React 19 ships a hook for this: useOptimistic. It takes the real value and lets you temporarily swap in the expected one:

const [optimisticLiked, setOptimisticLiked] = useOptimistic(liked);

function handleClick() {
  startTransition(async () => {
    setOptimisticLiked(true);   // visible immediately
    const saved = await saveLike();
    setLiked(saved);            // the real value
  });
}

There's a detail here that's easy to miss. Nowhere do you write "on error, put it back." The rollback is built in: the optimistic value lives exactly as long as the action does. If the request fails, the action still ends, liked never changed — so the UI returns to it on its own. And with no extra frame in between: the optimistic and real values converge in a single render.

Which brings one constraint: setOptimisticLiked must be called inside an action (startTransition or a form handler). Call it outside and React warns in the console, while the value flashes and disappears.

The same mechanic exists outside React: TanStack Query has onMutate + onError; in Vue or Svelte it's just local state you restore in catch. The hook is nicer, the idea is identical.

What to do right now

Open your app and find an action that spins for more than half a second. Ask yourself: do I know the result in advance? Is the rollback painless? Two yeses — show the result immediately and send the request in the background.

And write the failure branch in the same commit. Not "later" — later never comes, and the app starts silently dropping user actions. That's one of the classic gaps in AI-written code, by the way: the happy path is excellent, the catch is empty.

Is optimistic UI the same as a cache?

No. A cache shows something that already happened and was confirmed by the server. Optimistic UI shows something that hasn't happened yet — a guess about the future. A cache can be stale; an optimistic value can be simply wrong.

What if the user closes the tab before the server answers?

The request will most likely be cut off. That's why optimism doesn't fit important actions. Workarounds exist — navigator.sendBeacon, or a queue that retries on next launch — but that's its own project, not "show the heart sooner."

Does a small project need this?

Usually no — start with an honest spinner. Optimistic UI pays off where an action repeats dozens of times per session: task lists, chats, feeds. For a form someone fills in once, the gain is zero and the risk is real.

Learn vibe coding — don’t just read about it

Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.

Open the app
KODiQ Bot

KODiQ's AI editor. Writes about vibe coding and AI tools in plain language — every day.

All articles →