Chapter 4 of 4

Optimistic Updates

Showing the result before the server confirms it.

Waiting for a round trip makes an interface feel slow even when it is fast. An optimistic update shows the expected result immediately and reconciles when the server responds.

import { useOptimistic, useState } from "react";

function MessageList({ messages, onSend }) {
  const [optimisticMessages, addOptimistic] = useOptimistic(
    messages,
    (current, newMessage) => [...current, { ...newMessage, isSending: true }]
  );

  async function handleSend(text) {
    addOptimistic({ id: "temp", text });
    await onSend(text);   // when this resolves, messages updates for real
  }

  return (
    <ul>
      {optimisticMessages.map((message) => (
        <li key={message.id} style={{ opacity: message.isSending ? 0.5 : 1 }}>
          {message.text}
        </li>
      ))}
    </ul>
  );
}

React discards the optimistic state automatically once the underlying value updates, so there is no manual rollback to write for the success path.

When optimism is appropriate

  • The action almost always succeeds - liking, toggling, adding a comment.
  • Failure is easy to communicate and easy to undo.
  • The user would otherwise stare at a spinner for an action they expect to be instant.

Handling failure

async function handleSend(text) {
  addOptimistic({ id: "temp", text });
  try {
    await onSend(text);
  } catch (error) {
    // The optimistic entry disappears on the next render; tell the user why
    showToast("Message failed to send. Try again.");
  }
}

What you have learned

  • Suspense declares a fallback for a subtree; something must actually suspend for it to appear.
  • lazy splits the bundle - declare it at module level and preload on hover.
  • useTransition marks updates as interruptible so urgent ones stay instant.
  • useDeferredValue does the same when you only have the value, not the setter.
  • useOptimistic shows the expected result immediately for actions that rarely fail.

Decide whether content is stale

Complete isStale so it returns true when the current value differs from the deferred one.