10LOC
Reactadvanced

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; optimisticPost is what the component actually renders.
  • setOptimisticPost(nextLiked) must run inside a transition — here, the async function passed to startTransition — which is what tells React "this state is provisional until the transition settles."
  • Once toggleLike resolves and the transition completes, React re-renders using the real post prop (presumably updated by the caller by then); optimisticPost converges back to it automatically, with no explicit "clear the optimistic value" step.
  • If toggleLike throws, the transition still settles, and optimisticPost reverts to the real post — the UI snaps back to reflect that the action didn't actually happen.

Gotchas

  • Calling setOptimisticPost outside 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 keeps lineWaiver: 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.
  • post itself has to actually update once toggleLike succeeds (e.g., via revalidation or a server-sent update), or optimisticPost will revert to the same stale value it started from.

Related

  • useTransitionuseOptimistic is built on the same Actions mechanism; reach for useTransition alone 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.