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.
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:
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 itemposts— 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
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.
<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:
Warning: An optimistic update occurred outside a transition or action.The fix is wrapping in startTransition:
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.
Keep reading
Enjoyed this? Get more like it.
Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.