Optimistic Updates with useOptimistic

useOptimistic shows the result of an action before the server confirms it — making mutations feel instant even on a slow connection.

March 22, 20265 min read4 / 5

Slow network requests are a different category of performance problem. The computation isn't slow — the round trip to the server is. And unlike rendering, you can't make the internet faster.

What you can do is lie to the user. Politely.

Optimistic updates show the result of an action immediately — before the server confirms it. If the request succeeds, the real data replaces the optimistic version. If it fails, the UI reverts. The user gets instant feedback in the 99% case, and a clear error in the rare failure case.

Twitter's web app used this pattern to increase engagement by 25–50%. It's not new. What's new is that React 19 makes it trivial.

The Old Way

Before useOptimistic, you'd wire this up manually: push a fake item into the list, track its temporary ID, wait for the promise to resolve, replace the fake item with the real one, handle the error path by filtering out the temporary item. Doable, but tedious.

useOptimistic

The hook takes the real state and a reducer-style update function:

TSX
const [optimisticPosts, addOptimisticPost] = useOptimistic( posts, (currentState, newPost) => [newPost, ...currentState] );
  • optimisticPosts — the state to render (may include pending optimistic items)
  • addOptimisticPost — a function to add an optimistic item
  • posts — the real state; kept separate and preserved internally
  • The second argument is a function like a reducer: given current state and a new item, return the next optimistic state

When addOptimisticPost is called, optimisticPosts immediately includes the new item. But posts (the real state) is unchanged. React holds onto both — if the promise fails, it discards the optimistic layer and falls back to the real state automatically.

A Complete Example

TSX
function PostFeed() { const [posts, setPosts] = useState<Post[]>([]); const [optimisticPosts, addOptimisticPost] = useOptimistic( posts, (currentState, newPost: OptimisticPost) => [newPost, ...currentState] ); async function handleCreatePost(formData: FormData) { const title = formData.get('title') as string; const body = formData.get('body') as string; // Optimistic item — shown immediately const tempPost: OptimisticPost = { id: crypto.randomUUID(), // temporary ID title, body, userId: currentUserId, isPending: true, // React adds this flag automatically }; // Must be inside a transition startTransition(() => { addOptimisticPost(tempPost); }); try { // Slow network request happens in the background const created = await createPost({ title, body }); // Replace posts with real data (filters out the optimistic item) setPosts(prev => [created, ...prev.filter(p => p.id !== tempPost.id)]); } catch { // Real state is restored automatically by React // Show an error notification here } } return ( <ul> {optimisticPosts.map(post => ( <PostCard key={post.id} post={post} style={{ opacity: post.isPending ? 0.6 : 1 }} /> ))} </ul> ); }

The user submits the form. The post appears in the list immediately with a faded style. The real request happens in the background. When it resolves, the real post (with the server-assigned ID) takes over. The fade goes away.

The isPending Flag

React automatically adds an isPending: true flag to every optimistic item. You can use this to style pending items differently — a spinner, reduced opacity, a "Sending…" label, whatever fits the UX.

TSX
<PostCard post={post} className={post.isPending ? 'opacity-60 animate-pulse' : ''} />

The Transition Requirement

The call to addOptimisticPost must happen inside a transition. React will warn you if it doesn't:

Plain text
Warning: An optimistic update occurred outside a transition or action.

The fix is wrapping in startTransition:

TSX
startTransition(() => { addOptimisticPost(tempPost); });

Or using the useTransition hook. This is React connecting the optimistic update to the same priority lane system — the optimistic render is scheduled appropriately alongside the async work.

When to Use Optimistic Updates

Not everything should be optimistic. The pattern works when:

  • The happy path is nearly certain — form submissions, likes, follows, settings changes. These fail rarely. Showing them immediately is safe.
  • The action is clearly reversible — if something goes wrong, you can remove the item from the UI and show an error without lasting damage.
  • Perceived speed matters more than perfect accuracy — social actions, chat messages, checklist items. The user wants to feel heard immediately.

Avoid it when:

  • The result is uncertain (a payment, a booking with limited availability)
  • Failure has meaningful consequences the user needs to see immediately
  • The optimistic state would be significantly different from the real state

The Same Idea, Simpler Code

The concept here isn't new. jQuery apps were doing optimistic updates in 2012. What React 19 adds is a clean API that handles the hard parts automatically — preserving the real state, reverting on failure, integrating with the transition system for correct scheduling.

The network request wasn't made any faster. The experience for the user feels instant. That's the trade-off — and for most CRUD operations, it's the right one to make.

Practice what you just read.

Instant Feedback with useOptimistic
1 exercise

Enjoyed this? Get more like it.

Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.