Back to React
React
medium
mid

How do you use React's useOptimistic hook for instant UI updates?

`useOptimistic(state, reducer)` returns `[optimisticState, addOptimistic]`. Call `addOptimistic(action)` inside a transition (typically before `await api.submit()`); the UI shows the optimistic state immediately; once the underlying state updates (or the transition finishes), `optimisticState` reverts to derived-from-real. Rollback on error is automatic. Pairs with Server Actions in React 19.

4 min read·~15 min to think through

useOptimistic is React 19's built-in primitive for optimistic UI — show a guessed-future state immediately, reconcile with the real state when it arrives. Before it, people hand-rolled the pattern; now it's first-class.

The API

jsx
const [optimisticState, addOptimistic] = useOptimistic(
  realState,
  (current, action) => nextState     // reducer applied to current
);
  • realState — the source-of-truth state.
  • The reducer combines current optimistic state with an action.
  • addOptimistic(action) — must be called inside a transition.

Example — adding a comment

jsx
function CommentList({ postId, comments }) {
  const [optimisticComments, addOptimistic] = useOptimistic(
    comments,
    (current, newComment) => [...current, { ...newComment, pending: true }]
  );

  async function submit(formData) {
    const text = formData.get("text");
    const newComment = { id: crypto.randomUUID(), text, author: "me" };
    addOptimistic(newComment);                          // optimistic
    await postComment(postId, newComment);              // server
    // realState updates via revalidation / router refresh
  }

  return (
    <>
      {optimisticComments.map((c) => (
        <li key={c.id} style={{ opacity: c.pending ? 0.5 : 1 }}>{c.text}</li>
      ))}
      <form action={submit}>
        <input name="text" />
        <button>Post</button>
      </form>
    </>
  );
}

How it works

  1. addOptimistic is called inside the form's async action (an implicit transition).
  2. React renders optimisticComments = reducer(realComments, action).
  3. The component shows the comment instantly with pending: true styling.
  4. When the action resolves and the route revalidates, realState (comments) updates.
  5. optimisticState reverts to be derived from the new realState (no more optimistic action applied).

Why this beats hand-rolling

Without useOptimistic:

jsx
const [items, setItems] = useState(realItems);
const add = async (item) => {
  setItems((prev) => [...prev, item]);
  try { await api.add(item); }
  catch { setItems((prev) => prev.filter((i) => i !== item)); }
};

You have to manage rollback yourself, and items drifts from realItems if the source changes for other reasons.

useOptimistic always derives from the source — no drift. Rollback is automatic when the action ends.

Where it lives in React 19

Designed to pair with Server Actions and the app router model:

  • Server action triggers a mutation.
  • The router revalidates / refetches.
  • useOptimistic paints the intermediate UI.
  • When the revalidation completes, real state replaces optimistic.

Use cases

  • Adding a comment / message.
  • Liking a post.
  • Reordering a list.
  • Status changes (mark as done, mark read).
  • Cart add/remove.

Pitfalls

  • Must call inside a transition — typically inside a server action or startTransition. Calling outside throws.
  • Don't store complex state machines in optimistic state — keep it as transformations of the source state.
  • Error reconciliation isn't automatic in all cases — if the server returns a different value than predicted, you'll see a flash when the source updates.
  • Tagging pending items — useful for opacity / spinner; the reducer adds the flag.

Comparison to React Query optimistic updates

React Query has onMutate for optimistic updates with explicit rollback. useOptimistic is simpler but requires the React 19 model (transitions, server actions). For non-RSC apps, React Query is still the right tool.

Interview framing

"useOptimistic(state, reducer) returns [optimisticState, addOptimistic]. addOptimistic(action) inside a transition applies the reducer to compute an optimistic next state — that's what renders. When the underlying state updates (from revalidation or refetch) or the transition completes, optimistic state reverts to derived-from-source automatically. So you get instant UI without managing rollback yourself, and no drift between optimistic and real state. Designed for React 19's Server Actions model — pairs with the action triggering a mutation and the router revalidating. Pitfalls: must be inside a transition, keep the reducer pure, and handle visual reconciliation if the server's actual value diverges from your prediction."

Follow-up questions

  • Why must addOptimistic be inside a transition?
  • How does rollback work without explicit error handling?
  • Compare to React Query's onMutate pattern.
  • When does the optimistic state revert to real state?

Common mistakes

  • Calling addOptimistic outside a transition.
  • Treating optimistic state as a separate source of truth.
  • Forgetting to pass realState — optimistic drifts.
  • Heavy mutations in the reducer (it runs on every render).

Performance considerations

  • Reducer runs on every render — keep it cheap. Avoid deep clones; produce minimal diffs.

Edge cases

  • Multiple optimistic actions queued.
  • Server returns different value than predicted (visual flash).
  • Error during the action — UI reverts but error display is your job.

Real-world examples

  • Likes / comments / cart in Next.js App Router with Server Actions.
  • Vercel commerce-style demos.

Senior engineer discussion

Seniors pair useOptimistic with revalidation, keep reducers pure and small, handle the prediction-vs-reality flash deliberately, and reach for React Query in non-RSC apps where the SA model isn't available.

Related questions