How do you use React's useOptimistic 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.
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
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
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
addOptimisticis called inside the form's async action (an implicit transition).- React renders
optimisticComments= reducer(realComments, action). - The component shows the comment instantly with
pending: truestyling. - When the action resolves and the route revalidates,
realState(comments) updates. optimisticStatereverts to be derived from the newrealState(no more optimistic action applied).
Why this beats hand-rolling
Without useOptimistic:
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.
useOptimisticpaints 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.