Skip to content
GeeksSmith
advanced 7 min read

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

High-tier tech interviews (Walmart, Amazon, Meta, ConveGenius) evaluate how you build sub-100ms perceived latency for user actions (like toggles, upvotes, task reordering) without corrupting application state during race conditions or network failures.

Visual & Interactive Explanation

Optimistic Update with Rollback Lifecycle

Sequence & Data Exchange Flow

User
Client UI / Cache
API Server
#1
UserClient UI / Cache
request

Click 'Like' Button

#2
Client UI / CacheClient UI / Cache
compute

Snapshot previous state (liked: false)

#3
Client UI / CacheUser
render

Render 'Liked' immediately (0ms delay)

#4
Client UI / CacheAPI Server
request

POST /api/like

#5
API ServerClient UI / Cache
response

500 Internal Server Error

#6
Client UI / CacheClient UI / Cache
compute

Restore snapshot (liked: false)

#7
Client UI / CacheUser
render

Revert UI & show 'Action failed. Tap to retry.'

Code Examples & Implementation

React 19 Native useOptimistic Hook
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>
  );
}
Vanilla Zustand Optimistic Item Removal with Rollback
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?

Question 1hard
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:

Rate your confidence: