useOptimistic Explained: Instant UI During Server Updates

useOptimistic shows an instant, temporary UI while a mutation runs, then converges to the real result when the action finishes.

7 min read

useOptimistic lets a component show an instant, temporary value while a mutation runs, then converge with the real result when the action finishes. It turns a slow network call into immediate feedback: the UI flips now, and React reconciles the display once the server responds. It works with any transition, not just network calls, so the same pattern powers pending buttons and form feedback.

Stable in React 19

useOptimistic is stable since React 19. The setter only takes effect inside an Action, which is a function wrapped in startTransition or an Action prop.

The basic pattern

Call useOptimistic with the current value. It returns the optimistic state and a setter to change it for the duration of an Action.

App.jsxApp.jsx
import { useOptimistic, startTransition } from "react";
 
function LikeButton({ isLiked, toggleLike }) {
  const [optimisticLiked, setOptimisticLiked] = useOptimistic(isLiked);
 
  function handleClick() {
    startTransition(async () => {
      setOptimisticLiked(!optimisticLiked);
      await toggleLike();
    });
  }
 
  return (
    <button onClick={handleClick}>
      {optimisticLiked ? "Unlike" : "Like"}
    </button>
  );
}

The button label flips the instant it is clicked, before the server call finishes. The parent updates the real isLiked prop on success, and the optimistic value converges to it in the same render, so there is no extra clearing step.

A disabled button during the request is the simplest version of the same idea. For the server call itself, see React Server Functions and Server Actions.

The value argument is what renders when nothing is pending, so it remains the source of truth after the action ends.

The setter runs inside an Action

The optimistic setter must be called inside an Action. Calling it in a plain event handler logs a warning and the value briefly appears, then reverts because nothing holds it.

App.jsxApp.jsx
function handleClick() {
  startTransition(async () => {
    setOptimisticLiked(true);
    await toggleLike();
  });
}

startTransition holds the optimistic state until the async work finishes. Form action props already run inside a transition, so a setter called from a form action works without the wrapper. Treat the setter like a state update that only has meaning while a transition is open.

A Server Function that returns the saved value closes the loop between the guess and the answer, and the optimistic state simply hands over to that result.

Optimistic lists with a reducer

For lists, pass a reducer so React can reapply the pending change if the base list updates while the action runs.

App.jsxApp.jsx
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
  todos,
  (current, newTodo) => [...current, { ...newTodo, pending: true }]
);
 
function handleAdd(text) {
  const newTodo = { id: crypto.randomUUID(), text };
  startTransition(async () => {
    addOptimisticTodo(newTodo);
    await saveTodo(newTodo);
  });
}

The new todo appears instantly with a pending flag. When saveTodo finishes, the parent replaces the list with the saved items, and the pending item resolves to the confirmed one.

The reducer matters because another update could change the list while this one is in flight, and React replays the reducer against the latest list. For the form variant that tracks pending state, see React 19 Form Actions and useActionState.

Rollback when the action fails

If the action throws, the optimistic value does not stick. The base value never changed, so React renders it again and the UI rolls back automatically.

App.jsxApp.jsx
startTransition(async () => {
  removeItem(id);
  try {
    await deleteAction(id);
  } catch (error) {
    setError(error.message);
  }
});

The item reappears when the delete fails, and the error message shows why. Catch the error to give the user feedback instead of leaving them with a silently restored row.

Optimistic updates are a guess, so the failure path is part of the design rather than an edge case. Clear the error on the next attempt so the message disappears when the user retries.

Rollback is automatic because the base value never changed, so the optimistic state has nothing left to show.

When not to use it

useOptimistic only helps when the eventual truth is a value you already have, like a list or a flag. It does not replace loading states for work that has no optimistic projection, and it does not cache or fetch data on its own. See How to Handle Loading, Error, Empty, and Success States for the full async UI picture, and lean on a Server Function for the mutation itself.

Keep the projection close to the truth, and the optimistic UI will rarely need correction.

Rune AI

Rune AI

Key Insights

  • useOptimistic returns optimistic state and a setter.
  • The setter must run inside an Action.
  • Use a reducer when the base state may change mid-action.
  • A failed action rolls the optimistic state back.
  • It pairs with Server Functions for instant form feedback.
RunePowered by Rune AI

Frequently Asked Questions

Where must I call the optimistic setter?

Inside an Action, meaning inside startTransition or an Action prop. Calling it elsewhere logs a warning and the optimistic value reverts immediately.

What happens if the server update fails?

The action still ends, and React renders the current base value. Since the parent usually only updates the base value on success, the UI rolls back to its previous state.

Conclusion

useOptimistic shows a temporary value while a mutation runs and converges with the real state when the action finishes. Use a reducer for lists so concurrent updates stay correct.