Optimistic UI Updates & Rollback Strategies
Optimistic UI is an interaction design and architectural pattern where the client updates local state immediately assuming a server mutation will succeed, and cleanly rolls back state with user feedback if the mutation fails.
Why it matters in interviews
Visual & Interactive Explanation
Optimistic Update with Rollback Lifecycle
Sequence & Data Exchange Flow
Click 'Like' Button
Snapshot previous state (liked: false)
Render 'Liked' immediately (0ms delay)
POST /api/like
500 Internal Server Error
Restore snapshot (liked: false)
Revert UI & show 'Action failed. Tap to retry.'
Code Examples & Implementation
import { useOptimistic, useState, useTransition } from "react";
interface Message {
id: string;
text: string;
sending?: boolean;
}
export function ChatThread({ initialMessages }: { initialMessages: Message[] }) {
const [messages, setMessages] = useState<Message[]>(initialMessages);
const [isPending, startTransition] = useTransition();
// Optimistic layer over real messages
const [optimisticMessages, setOptimisticMessages] = useOptimistic(
messages,
(state, newMessage: string) => [
...state,
{ id: crypto.randomUUID(), text: newMessage, sending: true },
]
);
async function handleSend(formData: FormData) {
const text = formData.get("text") as string;
if (!text.trim()) return;
startTransition(async () => {
// 1. Immediately visible with sending: true
setOptimisticMessages(text);
try {
const savedMessage = await apiSendMessage(text);
// 2. Real state updated on success
setMessages((prev) => [...prev, savedMessage]);
} catch (err) {
// Automatic rollback because optimisticMessages derives from messages state
alert("Failed to send message.");
}
});
}
return (
<div>
<ul>
{optimisticMessages.map((m) => (
<li key={m.id} style={{ opacity: m.sending ? 0.6 : 1 }}>
{m.text} {m.sending && "⏳"}
</li>
))}
</ul>
<form action={handleSend}>
<input name="text" placeholder="Type a message..." />
<button type="submit">Send</button>
</form>
</div>
);
}interface ItemStore {
items: Array<{ id: string; name: string }>;
deleteItem: (id: string) => Promise<void>;
}
export const useItemStore = create<ItemStore>((set, get) => ({
items: [],
deleteItem: async (id: string) => {
// 1. Snapshot previous state
const previousItems = get().items;
// 2. Optimistic removal
set({ items: previousItems.filter((item) => item.id !== id) });
try {
const res = await fetch(`/api/items/${id}`, { method: "DELETE" });
if (!res.ok) throw new Error("Delete failed");
} catch (error) {
// 3. Rollback on failure
set({ items: previousItems });
toast.error("Could not delete item. Restoring...");
}
},
}));Interview-Ready Answers
Optimistic updates modify the UI instantly before network confirmation, maintaining a rollback snapshot in case of rejection to ensure zero perceived latency.
Follow-up Questions & Deep Dives
🎯 Code Output — What Does This Print?
let count = 10;
async function mutateCount(newVal: number) {
const snapshot = count;
count = newVal; // optimistic
console.log("optimistic:", count);
try {
throw new Error("Server rejected");
} catch {
count = snapshot; // rollback
console.log("rollback:", count);
}
}
mutateCount(20);Common Mistakes & Anti-Patterns
- Not snapshotting state before applying optimistic mutations, making clean rollback impossible
- Failing to notify the user when an optimistic action was reverted
- Using optimistic updates on high-concurrency shared documents without conflict resolution logic (OT/CRDT)
- Failing to cancel in-flight queries before optimistic mutations, causing network responses to overwrite the optimistic state
Real-World Architectural Scenario
In a Kanban board application, dragging a ticket to 'Done' updates instantly. But when offline, the ticket jumps back without warning 10 seconds later, confusing the user. How do you design this?
Rate Your Readiness
Rate how comfortably you can explain this in an interview to update your global readiness gauge: