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.
The core optimistic UI pattern is always three steps: update local state immediately so the user sees the change at zero latency, fire the async mutation in the background, and revert local state if the mutation fails. This three-step contract is so reliable and repeatable that every major UI library — React Query, SWR, Apollo, Zustand — has first-class support for it. Understanding the raw useState version first gives you the mental model to use those abstractions correctly and to debug them when they go wrong. The revert step is the part most developers initially skip, and it is the part that matters most when a real failure occurs.
A Like button that toggles instantly via useState, fires a fetch in the background, and reverts with an error message if the server rejects the mutation. Both the optimistic path and the rollback path are visible.
0.2 to 0.8 and click the button rapidly — observe how the rollback correctly restores liked to whatever previous was captured as at the time of each click.setError(null) call to after the setLiked(!previous) line and verify the error message clears before the new request resolves, not after.42 → 43 optimistically) and verify the rollback restores the exact pre-click count even after multiple fast clicks.console.log('rollback to', previous) inside the catch block and confirm in the console that the logged value matches the button's displayed state after each failed click.Use these three in order. Each builds on the one before.
In one paragraph, describe the three steps of the optimistic UI pattern and what each step accomplishes for the user experience.
Walk me through how React's state batching interacts with the optimistic update and rollback sequence — can two state updates in the same event handler cause a visible flicker?
If a user clicks a Like button five times in rapid succession before any server response arrives, how does the three-step optimistic pattern behave, and what additional logic would you need to handle this correctly?
import { useState } from "react";
async function fakeLikeAPI(liked: boolean): Promise<void> {
await new Promise((r) => setTimeout(r, 600));
if (Math.random() < 0.2) throw new Error("Server error");
}
export function LikeButton() {
const [liked, setLiked] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleLike() {
const previous = liked;
setError(null);
setLiked(!previous); // step 1: optimistic update
try {
await fakeLikeAPI(!previous); // step 2: async mutation
} catch (e) {
setLiked(previous); // step 3: rollback
setError("Could not save — please try again.");
}
}
return (
<div>
<button onClick={handleLike}>{liked ? "❤️ Liked" : "🤍 Like"}</button>
{error && <p style={{ color: "red" }}>{error}</p>}
</div>
);
}