"Optimistic UI" — update the screen before the server responds so the app feels instant — used to mean hand-rolling a temporary state and a manual rollback on error. React 19's useOptimistic bakes it into the framework. So I built a tiny chat where you can watch the instant update and the automatic revert.
▶ Live demo: https://useoptimistic-lab.vercel.app/
Source (React 19): https://github.com/dev48v/useoptimistic-lab
Send a message: the bubble appears immediately as sending, then commits to ✓ sent when the fake server confirms. Flip "next send fails" and watch the bubble simply vanish — no rollback code.
The hook
const [optimistic, addOptimistic] = useOptimistic(
messages, // real, committed state
(cur, text) => [...cur, { text, status: "sending" }] // how to show a pending change
);
startTransition(async () => {
addOptimistic(text); // 1. render instantly from optimistic state
const saved = await save(text); // 2. the real request
setMessages(m => [...m, saved]); // 3. commit → optimistic reconciles into real
// on throw: no commit → React discards the optimistic value and snaps back
});
Three things happen:
-
Instant —
addOptimisticderives a temporary state that renders right away; the user never waits on the network. -
Reconcile — when the action commits the real state (
setMessages), the optimistic entry is replaced by the confirmed one ("sending…" → "✓ sent"). - Auto-rollback — if the async action finishes without committing (it threw), React throws the optimistic value away and the UI returns to the real state. The failed bubble disappears on its own.
The mental model that makes it click
The revert isn't something you code — it's driven by whether the underlying state actually changed by the time the action (transition) ends. Commit → the optimistic value merges in. Don't commit → it's discarded. That's why there's no catch block restoring old state anywhere in the example.
One gotcha: useOptimistic has to be updated inside a transition or a form action, so React knows when the optimistic window opens and closes. The demo uses startTransition with an async function; with <form action> + useActionState it's even shorter.
If this made optimistic UI click, a star helps others find it: https://github.com/dev48v/useoptimistic-lab
Top comments (0)