Optimistic UI

useOptimistic

For actions that almost always succeed (like, add to wishlist, add to cart), show the result immediately and let the server confirm. React 19's useOptimistic shows a temporary state while the action runs and drops it when the real data arrives.

'use client';

import { useOptimistic, startTransition } from 'react';
import { toggleWishlist } from './actions';

export default function WishlistButton({ productId, saved }: { productId: number; saved: boolean }) {
  const [optimisticSaved, setOptimisticSaved] = useOptimistic(saved);

  function onClick() {
    startTransition(async () => {
      setOptimisticSaved(!optimisticSaved); // instant
      await toggleWishlist(productId); // server; page revalidates with the real value
    });
  }

  return <button onClick={onClick}>{optimisticSaved ? '♥ Saved' : '♡ Save'}</button>;
}

Optimistic List Updates

Example

The core of optimistic updates is a pure reducer. On success the server data replaces it; on failure you roll back to the confirmed state:

type Item = { id: number; name: string; qty: number; pending?: boolean };
type Action = { type: 'add'; item: Item } | { type: 'remove'; id: number };

function optimistic(state: Item[], action: Action): Item[] {
  switch (action.type) {
    case 'add':
      return [...state, { ...action.item, pending: true }];
    case 'remove':
      return state.filter((i) => i.id !== action.id);
  }
}

const confirmed: Item[] = [{ id: 1, name: 'Clean Code', qty: 1 }];
const shown = optimistic(confirmed, { type: 'add', item: { id: 2, name: 'USB-C Cable', qty: 2 } });
console.log('while saving:', shown.map((i) => i.name + (i.pending ? ' (saving)' : '')).join(', '));

const serverFailed = true;
const finalState = serverFailed ? confirmed : shown.map(({ pending, ...rest }) => rest);
console.log('after error :', finalState.map((i) => i.name).join(', '));

Output

while saving: Clean Code, USB-C Cable (saving)
after error : Clean Code