Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Optimistic UI is powerful precisely because it trades a guarantee for perceived speed — and that trade is only acceptable when the failure rate is low and the consequence of a rollback is minor. Applying it to financial transactions, irreversible deletes, or form submissions with complex validation creates a user experience that feels broken: the UI says 'done' and then takes it back, leaving the user unsure what actually happened. The decision heuristic is simple: if the mutation has a greater than 95% success rate and a revert is visually cheap and semantically reversible, go optimistic. Otherwise, use a spinner and block further interaction until you have a confirmed result.
Two side-by-side interactions: a pessimistic checkout button that shows a spinner until the server confirms, and an optimistic like button that toggles instantly. The contrast makes the appropriate use cases obvious.
Math.random() < 0.5 failure to CheckoutButton and observe how the missing rollback leaves the UI in a 'done' state even though the payment failed — note why this is dangerous.CheckoutButton to rollback (setStatus('idle')) on failure with an error message, then decide whether this changes your opinion on whether checkout should be optimistic.LikeButton pessimistically (disable the button until the fetch resolves) and time the difference in perceived responsiveness using performance.now().Use these three in order. Each builds on the one before.
In one paragraph, explain the decision criteria for choosing optimistic UI over a loading spinner for a given user action.
Walk me through how a high-traffic social platform evaluates whether a new mutation type (say, a 'Bookmark' feature) is safe to make optimistic.
A user profile update form has 12 fields, some of which trigger server-side validation the client cannot replicate. Design a hybrid strategy that is optimistic for low-risk fields and pessimistic for fields that frequently fail validation.
import { useState } from "react";
// Pessimistic: appropriate for checkout
function CheckoutButton() {
const [status, setStatus] = useState<"idle" | "pending" | "done">("idle");
async function handleCheckout() {
setStatus("pending");
await new Promise((r) => setTimeout(r, 1200)); // payment API
setStatus("done");
}
if (status === "done") return <p>Order confirmed!</p>;
return (
<button disabled={status === "pending"} onClick={handleCheckout}>
{status === "pending" ? "Processing..." : "Pay $49"}
</button>
);
}
// Optimistic: appropriate for social actions
function LikeButton() {
const [count, setCount] = useState(128);
const [liked, setLiked] = useState(false);
async function handleLike() {
const prev = { count, liked };
setLiked(!liked);
setCount((c) => (liked ? c - 1 : c + 1));
try {
await new Promise((r) => setTimeout(r, 600));
} catch {
setLiked(prev.liked);
setCount(prev.count);
}
}
return (
<button onClick={handleLike}>
{liked ? "❤️" : "🤍"} {count}
</button>
);
}