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 optimistic/pessimistic choice is not binary. In practice, most production UIs operate on a spectrum: fully pessimistic (block until confirmed), partially optimistic (update UI but show a pending indicator), and fully optimistic (update UI with no pending indicator, silent network call). Each point on the spectrum has clear appropriate uses. Fully optimistic is right for social actions where failure is rare and the rollback cost is low. Partially optimistic is right for content edits where you want to show the result immediately but signal that save is in progress. Fully pessimistic is right for irreversible or high-stakes operations. Recognizing where a given action falls on this spectrum, and being consistent within an application, is what separates polished UX from janky UX.
The same 'follow user' action implemented three ways: fully pessimistic (spinner, button disabled), partially optimistic (button updates, subtle pending dot), and fully optimistic (instant, silent).
import { useState } from "react";
async function followUser(id: string): Promise<void> {
await new Promise((r) => setTimeout(r, 800));
if (Math.random() < 0.1) throw new Error("failed");
}
// Fully pessimistic
export function PessimisticFollow({ userId }: { userId: string }) {
const [following, setFollowing] = useState(false);
const [loading, setLoading] = useState(false);
async function handleFollow() {
setLoading(true);
try {
await followUser(userId);
setFollowing((v) => !v);
} finally {
setLoading(false);
}
}
return (
<button onClick={handleFollow} disabled={loading}>
{loading ? "..." : following ? "Following" : "Follow"}
</button>
);
}
// Partially optimistic
export function PartialFollow({ userId }: { userId: string }) {
const [following, setFollowing] = useState(false);
const [pending, setPending] = useState(false);
async function handleFollow() {
const prev = following;
setFollowing(!prev);
setPending(true);
try {
await followUser(userId);
} catch {
setFollowing(prev);
} finally {
setPending(false);
}
}
return (
<button onClick={handleFollow}>
{following ? "Following" : "Follow"}
{pending && <span style={{ marginLeft: 4, opacity: 0.5 }}>•</span>}
</button>
);
}
// Fully optimistic
export function OptimisticFollow({ userId }: { userId: string }) {
const [following, setFollowing] = useState(false);
async function handleFollow() {
const prev = following;
setFollowing(!prev);
try {
await followUser(userId);
} catch {
setFollowing(prev);
}
}
return (
<button onClick={handleFollow}>
{following ? "Following" : "Follow"}
</button>
);
}0.5 and click each button five times — record how the user experience of failure differs across the three implementations.3000ms in PessimisticFollow and compare the perceived responsiveness — explain in one sentence why fully optimistic is clearly better at this latency.OptimisticFollow when the catch block fires — verify it is visible at the 10% default failure rate.data-testid attribute to each button and write a test assertion that verifies PessimisticFollow has disabled=true during the 800ms pending window.Use these three in order. Each builds on the one before.
In one paragraph, describe the three points on the optimistic-pessimistic spectrum and give one real product example that fits each point.
Walk me through how a partially optimistic UI differs from a fully optimistic UI at the state management level — what additional state variable is required and why?
A design review flags that your fully optimistic 'Follow' button occasionally shows a brief rollback flash that confuses users. Propose two strategies for reducing the perceived error rate without switching to pessimistic mode.