useOptimistic for instant UI feedback before the server confirms
Published May 11, 2027
import { useOptimistic, useTransition } from "react";
type Post = { id: string; likedByMe: boolean; likeCount: number };
export const LikeButton = ({ post, toggleLike }: { post: Post; toggleLike: (id: string) => Promise<void> }) => {
const [optimisticPost, setOptimisticPost] = useOptimistic(post, (current, liked: boolean) => ({
...current,
likedByMe: liked,
likeCount: current.likeCount + (liked ? 1 : -1),
}));
const [, startTransition] = useTransition();
const handleClick = () => {
const nextLiked = !optimisticPost.likedByMe;
startTransition(async () => {
setOptimisticPost(nextLiked);
await toggleLike(post.id);
});
};
return (
<button onClick={handleClick}>
{optimisticPost.likedByMe ? "♥" : "♡"} {optimisticPost.likeCount}
</button>
);
};What
useOptimistic shows a predicted result of an action immediately, then reconciles it with the real state once the action actually finishes — without a separate "pending" copy of the state to manage by hand.
Why it matters
Waiting for a server round-trip before updating a like button, a checkbox, or a comment list makes every action feel like it has latency, even on a fast connection — because it does. The manual alternative is a parallel piece of local state that mirrors the "optimistic" version, which you then have to carefully reset if the action fails or reconcile once the real response arrives — logic that's easy to get subtly wrong (double-counting, flashing back to the old value, forgetting to roll back on error). useOptimistic handles the reconciliation for you: its return value is the real post prop except while an action is pending, when it's whatever setOptimisticPost last computed.
How it works
useOptimistic(post, reducer)takes the current real state and a reducer that computes the optimistic version from it;optimisticPostis what the component actually renders.setOptimisticPost(nextLiked)must run inside a transition — here, theasyncfunction passed tostartTransition— which is what tells React "this state is provisional until the transition settles."- Once
toggleLikeresolves and the transition completes, React re-renders using the realpostprop (presumably updated by the caller by then);optimisticPostconverges back to it automatically, with no explicit "clear the optimistic value" step. - If
toggleLikethrows, the transition still settles, andoptimisticPostreverts to the realpost— the UI snaps back to reflect that the action didn't actually happen.
Gotchas
- Calling
setOptimisticPostoutside of a pending transition/Action throws — it's not a general-purpose "fake state" setter, it only makes sense paired with the async work it's predicting the result of. The transition wrapper and the reducer are both required to see this end-to-end, which is why the snippet keepslineWaiver: true. - On failure, the optimistic value reverts, but nothing shows why automatically — pair this with your own error state (or a toast) if the user needs to know the like didn't register, not just see it undo.
postitself has to actually update oncetoggleLikesucceeds (e.g., via revalidation or a server-sent update), oroptimisticPostwill revert to the same stale value it started from.
Related
useTransition—useOptimisticis built on the same Actions mechanism; reach foruseTransitionalone when you need a pending flag but have nothing to predict.- Server Actions with
useActionState— the same optimistic-then-reconcile shape shows up naturally when the "action" is a form submission instead of a button click.