Optimistic UI updates are a lie, but they’re a lie the user wants to believe.

Imagine a user clicks "Add to Cart" on your e-commerce site. Normally, the browser sends a request to the server, waits for a confirmation, and then updates the UI to show the item in the cart. This can take hundreds of milliseconds, or even seconds if the network is slow.

With optimistic UI, you skip the waiting. The moment the user clicks, you immediately update the UI as if the request already succeeded. The cart count jumps, the item appears, a confirmation message flashes. It feels instantaneous.

Here’s how it looks in action. Let’s say we have a simple addToCart function in React.

// State management (e.g., using Zustand or Redux)
const useCartStore = create((set) => ({
  items: [],
  addItem: async (item) => {
    // Optimistic update: Add item to UI immediately
    set((state) => ({ items: [...state.items, { ...item, optimisticId: Date.now() }] }));

    try {
      // Make the actual API call to the server
      const response = await fetch('/api/cart/add', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(item),
      });

      if (!response.ok) {
        throw new Error('Failed to add item to cart');
      }

      const addedItem = await response.json();

      // Success: Replace optimistic item with actual item from server
      set((state) => ({
        items: state.items.map(cartItem =>
          cartItem.optimisticId === item.optimisticId ? { ...addedItem, id: addedItem.id } : cartItem
        ),
      }));
    } catch (error) {
      // Failure: Rollback the optimistic update
      console.error("Cart update failed:", error);
      set((state) => ({
        items: state.items.filter(cartItem => cartItem.optimisticId !== item.optimisticId),
      }));
      // Optionally show an error message to the user
      alert("Could not add item to cart. Please try again.");
    }
  },
}));

function ProductPage({ product }) {
  const addItem = useCartStore((state) => state.addItem);

  const handleAddToCart = () => {
    addItem(product);
  };

  return (
    <div>
      <h2>{product.name}</h2>
      <button onClick={handleAddToCart}>Add to Cart</button>
    </div>
  );
}

The core idea is to decouple the UI update from the actual network response. When the user initiates an action (like clicking "Add to Cart"), you:

  1. Update the UI optimistically: Immediately change the state to reflect the expected outcome. For example, add a temporary item to the cart array. This temporary item often needs a unique identifier (like optimisticId in the example) so you can find and remove it later.
  2. Initiate the asynchronous operation: Make the actual API call to the server in the background.
  3. Handle the response:
    • On success: If the server confirms the action, you update the UI state to reflect the actual data from the server. This usually involves replacing the optimistically added item with the server-verified item, using the optimisticId to match them up.
    • On failure: If the server returns an error (e.g., item out of stock, network issue), you rollback the UI. This means removing the optimistically added item from the state, effectively undoing the instant update. You’d also typically show an error message to the user.

The optimisticId is crucial. It’s a temporary marker that lets you distinguish between an item that’s still being processed by the server and one that’s fully confirmed. When the server responds, you use this ID to find the corresponding optimistic entry and either replace it with the real data or remove it entirely if the operation failed.

Most people think of optimistic UI as just speeding up perceived load times. But it’s also a powerful pattern for managing complex, multi-step user interactions where intermediate states are temporary and prone to failure. The system doesn’t just pretend to succeed; it creates a transient state that can be gracefully corrected. This allows you to build applications that feel incredibly responsive, even when underlying operations are inherently asynchronous and potentially unreliable. It’s about managing user perception by providing immediate, actionable feedback, then resolving the truth asynchronously.

The next challenge is handling concurrent optimistic updates, especially when they involve dependencies or order sensitivity, like editing an item that was just added.

Want structured learning?

Take the full React course →