Skip to content

Optimistic UI

When to Use

Use optimistic UI when the expected server response is success and failure is rare. Skip it for destructive, financial, or irreversible actions.

Decision

Scenario Use Optimistic UI? Why
Toggle like/bookmark/follow Yes Failure rate < 1%; rollback is non-destructive
Reorder list items Yes User sees intent immediately; rollback is a re-sort
Add comment/post Yes with caution Show optimistic state; replace with server-returned item after confirm
Delete a record Maybe — with confirmation Rollback after deletion confuses users; consider "undo" toast instead
Payment/financial transaction No Consequences of incorrect optimistic state are serious
Destructive irreversible action No Users must wait for server confirmation
File upload No Progress indication is more honest than optimistic completion

Pending state indicators:

Approach When to Use Avoid When
Subtle opacity (0.6) on pending items Normal optimistic adds High-frequency actions — constant dimming looks broken
Spinner inside button Slow operations (> 1s) Instant operations — spinner flash is worse than nothing
No indicator Toggle states with < 100ms server response Slow connections — users retry a completed action

Pattern

// Optimistic toggle (like, bookmark, follow)
async function optimisticToggle(element, action) {
  const previous = element.dataset.state;
  const next = previous === 'liked' ? 'unliked' : 'liked';
  element.dataset.state = next;                         // Update UI immediately
  element.setAttribute('aria-pressed', next === 'liked');
  try {
    const result = await action(next);
    element.dataset.state = result.state;               // Replace with server canonical state
  } catch {
    element.dataset.state = previous;                   // Rollback
    element.setAttribute('aria-pressed', previous === 'liked');
    showErrorToast('Could not save — please try again');
  }
}

// Optimistic list item add
async function optimisticAdd(list, createFn, itemData) {
  const tempId = `temp-${Date.now()}`;
  const tempItem = renderItem({ ...itemData, id: tempId, pending: true });
  list.appendChild(tempItem);                          // Show immediately
  try {
    const saved = await createFn(itemData);
    list.replaceChild(renderItem(saved), tempItem);    // Replace temp with server response
  } catch {
    tempItem.remove();                                 // Rollback
    showErrorToast('Could not save — check your connection');
  }
}

Common Mistakes

  • Wrong: No rollback implementation → Right: Server failure silently corrupts UI state without rollback
  • Wrong: Rollback without error message → Right: Always show a non-blocking toast explaining the failure
  • Wrong: Blocking the UI while "confirming" → Right: Defeats the purpose; the whole point is non-blocking response
  • Wrong: Optimistic state for destructive actions → Right: User thinks data is deleted; rollback is confusing
  • Wrong: Using a temp ID and never replacing it → Right: Always reconcile with server-returned canonical ID
  • Wrong: Replacing rollback with page reload → Right: Use rollback state + toast; reload is extreme

See Also