DEV Community

Ahmed Mahmoud
Ahmed Mahmoud

Posted on • Originally published at devya.dev

useOptimistic in React 19: Patterns and Pitfalls from Real Forms

Headline: React 19's useOptimistic shows the result of an action before the server confirms it. The API is tiny; the mental model is where people trip. Here's what held up across a comment box, a like button, and a settings form — plus three edges that cost me an afternoon each.

The mental model

useOptimistic stores nothing durable. It's a view over your real state that is only live while a transition is pending.

const [optimistic, addOptimistic] = useOptimistic(
  realState,
  (current, action) => nextState
);
Enter fullscreen mode Exit fullscreen mode

The first arg is the source of truth; the second is a pure reducer. The instant your action resolves and realState updates, React throws the optimistic value away and re-renders from truth. You never manually roll back.

Pattern 1: append on submit

const [optimisticComments, addOptimistic] = useOptimistic(
  comments,
  (state, text: string) => [...state, { id: crypto.randomUUID(), text, pending: true }]
);
Enter fullscreen mode Exit fullscreen mode

The pending flag drives a dimmed style; the server action revalidates the list so it refreshes from source.

Pitfall 1: fire it inside a transition

Calling addOptimistic from a plain onClick makes the value flash and vanish — there's no pending transition to keep it alive. Always trigger it from a form action or startTransition.

Pattern 2: toggle with a count

const [state, toggle] = useOptimistic(
  { liked, count },
  (s) => ({ liked: !s.liked, count: s.count + (s.liked ? -1 : 1) })
);
Enter fullscreen mode Exit fullscreen mode

Because the reducer receives the current optimistic value, rapid double-clicks compose correctly instead of fighting a stale closure.

Pitfall 2: errors need their own signal

useOptimistic has no error channel. When the action throws, the optimistic value disappears — correct — but the user gets no explanation. Pair it with useActionState or a toast.

Pitfall 3: unique temporary keys

Reusing id: 'temp' for every pending row makes React merge concurrent entries. Give each optimistic item a unique temp key and let the server row replace it on revalidation.

When I skip it

For low-confidence writes — payments, destructive deletes, heavy validation — I keep an honest pending state. Showing success you can't guarantee erodes trust faster than a spinner.


Originally published on devya.dev. Also on eng-ahmed.com. Built by Devya Solutions.

Top comments (0)