Ch. 3 · React

React useOptimistic and Instant Feedback

Show the expected result immediately with useOptimistic, then reconcile with the server and roll back cleanly on failure.

~2 min readadvancedupdated Oct 5, 2026

useOptimistic renders a temporary value while an asynchronous action is in flight, so the interface reflects the user’s intent immediately. When the action settles, React discards the optimistic value and re-renders from the real state, which is either the new server data or the previous value on failure.

Before you start

You should be comfortable with state, async actions and useTransition. This article assumes React 19-style actions; the reconciliation idea applies to any optimistic UI.

Step-by-step walkthrough

Step 1: Keep real state as the source of truth

Pass the actual state to useOptimistic(state, reducer); the hook returns the optimistic view plus an updater. The real state never changes optimistically, so a failed action simply stops applying the update and the UI returns to reality.

Step 2: Apply the optimistic update inside the action

Call the updater inside the action callback, not during render. The update is scoped to the transition: while the action is pending the optimistic value is shown, and the value is reset automatically once the action completes or fails.

Step 3: Make the action idempotent and recover from failure

If the server call throws, React reverts to the base state, so the UI is consistent without manual rollback. Report the error separately, and ensure the action is safe to retry, since the user may repeat it after seeing a failure.

Worked scenario

The list shows the new item instantly and removes it if the action fails.

function TodoList({ todos, addTodo }) {
  const [optimistic, addOptimistic] = useOptimistic(todos, (state, text) => [
    ...state,
    { text, pending: true },
  ]);
  async function handleAdd(formData) {
    addOptimistic(String(formData.get('text')));
    await addTodo(String(formData.get('text')));
  }
  return (
    <form action={handleAdd}>
      <input name="text" />
      <ul>{optimistic.map((t) => <li key={t.text}>{t.text}</li>)}</ul>
    </form>
  );
}
TSX

Walk through the example

Submitting the form calls handleAdd, which first adds a pending item to the optimistic list, so it appears at once. Then addTodo runs; when it resolves, the real todos prop updates and the optimistic value is dropped. If addTodo rejects, the optimistic item disappears without any manual cleanup.

Common mistake

Treating the optimistic value as the source of truth and reading it elsewhere, which shows data the server has not accepted. Another is adding an optimistic item with a key that changes on reconciliation, causing a remount; use a stable key.

Verify the behavior

Delay the server response and confirm the item shows immediately with a pending style. Make the action reject and confirm the item vanishes. Submit the same item twice and confirm no duplicate remains once the real state arrives.

Interview exercise

Why does useOptimistic revert automatically instead of needing manual rollback?

Answer and reasoning

Because it never mutates the real state. The optimistic value exists only for the duration of the transition, layered on top of the base state. When the action finishes, the layer is discarded and the component renders the real state again, which is the server result on success or the old value on failure.

Continue learning

Compare pending UI in React transition urgency and state snapshots. Read the React useOptimistic reference and try the React interview questions.

More in React

read ✓React · hard

React Context Splitting for Performance

Stop a single context value from re-rendering every consumer by splitting state and dispatch and memoizing the provider value.

~2 min readread →
read ✓React · mid

React lazy Loading and Code Splitting

Split bundles with lazy and Suspense, work with default exports, and place boundaries so a chunk loads only when needed.

~2 min readread →
esc