useOptimistic in React 19: Snappy UI Updates Made Easy
Learn how React 19's useOptimistic hook makes implementing instant UI updates a breeze. From basic examples to production patterns, here's everything you need to know.
While I was looking over some production React code the other day, I stumbled across a comment that said "TODO: Add optimistic updates" followed by about 50 lines of manual state juggling. Little did I know this was about to become a perfect teaching moment for React 19's useOptimistic hook.
I was once guilty of writing the exact same kind of code. You know the pattern: click a like button, show a loading spinner, wait for the API response, then finally update the UI. It works, but it feels slow. Users notice that delay, even if it's just 200 milliseconds.
What is Optimistic UI and Why It Matters
Let me paint you a picture. You're scrolling through a social media app and you hit the like button on a post. The heart instantly turns red. No spinner. No delay. It just happens. That's optimistic UI in action.
Here's what's actually happening behind the scenes: the app assumes your request will succeed and updates the UI immediately. The API call still happens in the background, but the user doesn't wait for it. If something goes wrong (rare, but it happens), the app rolls back the change.
I cannot stress this enough! This pattern isn't just about making your app "feel faster." It fundamentally changes how users perceive your application's responsiveness. When I finally decided to implement this pattern consistently across my projects, I saw a noticeable drop in user complaints about "sluggish" interfaces.
Understanding React 19's useOptimistic Hook
Before React 19, implementing optimistic updates meant managing multiple pieces of state, handling rollbacks manually, and writing a lot of boilerplate. I've written that code more times than I care to admit.
React 19 changes the game with useOptimistic. This hook gives you a clean API for implementing optimistic updates without all the manual state management. Here's the basic signature:
const [optimisticState, addOptimistic] = useOptimistic(
actualState,
(currentState, optimisticValue) => {
// Return the optimistic state
return optimisticValue;
}
);The hook takes two arguments: your actual state and a reducer function that determines how to apply optimistic updates. It returns the optimistic state and a function to trigger updates.

Basic useOptimistic Implementation: Like Button Example
Let's start with a simple example that I actually pulled from a real project. Here's a like button that feels instant:
'use client';
import { useOptimistic, useState } from 'react';
interface Post {
id: string;
likes: number;
isLiked: boolean;
}
async function likePost(postId: string): Promise<void> {
const response = await fetch(`/api/posts/${postId}/like`, {
method: 'POST',
});
if (!response.ok) {
throw new Error('Failed to like post');
}
}
export function LikeButton({ post }: { post: Post }) {
const [optimisticPost, addOptimisticLike] = useOptimistic(
post,
(currentPost, isLiking: boolean) => ({
...currentPost,
likes: currentPost.likes + (isLiking ? 1 : -1),
isLiked: isLiking,
})
);
async function handleLike() {
const newLikedState = !optimisticPost.isLiked;
addOptimisticLike(newLikedState);
try {
await likePost(post.id);
} catch (error) {
// The optimistic state automatically rolls back on re-render
console.error('Failed to like post:', error);
}
}
return (
<button
onClick={handleLike}
className={optimisticPost.isLiked ? 'liked' : ''}
>
❤️ {optimisticPost.likes}
</button>
);
}Look at how clean this is! When users click the button, addOptimisticLike immediately updates the UI. The actual API call happens in the background. If it fails, React automatically rolls back to the real state on the next render.
In other words, you get instant feedback without sacrificing data integrity. Wonderful!
Real-World Example: Comment System with Rollback
Here's where things get more interesting. I came across a comment system recently that needed optimistic updates with proper error handling. This example shows you how to handle more complex scenarios:
'use client';
import { useOptimistic, useState } from 'react';
interface Comment {
id: string;
text: string;
author: string;
isPending?: boolean;
}
async function postComment(text: string): Promise<Comment> {
const response = await fetch('/api/comments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
if (!response.ok) {
throw new Error('Failed to post comment');
}
return response.json();
}
export function CommentSection({ initialComments }: { initialComments: Comment[] }) {
const [comments, setComments] = useState(initialComments);
const [error, setError] = useState<string | null>(null);
const [optimisticComments, addOptimisticComment] = useOptimistic(
comments,
(currentComments, newComment: Comment) => {
return [...currentComments, { ...newComment, isPending: true }];
}
);
async function handleSubmit(formData: FormData) {
const text = formData.get('comment') as string;
setError(null);
const tempComment: Comment = {
id: `temp-${Date.now()}`,
text,
author: 'You',
};
addOptimisticComment(tempComment);
try {
const newComment = await postComment(text);
setComments((prev) => [...prev, newComment]);
} catch (err) {
setError('Failed to post comment. Please try again.');
// Optimistic state automatically rolls back here
}
}
return (
<div>
{error && <div className="error">{error}</div>}
<div className="comments">
{optimisticComments.map((comment) => (
<div
key={comment.id}
className={comment.isPending ? 'pending' : ''}
>
<strong>{comment.author}:</strong> {comment.text}
</div>
))}
</div>
<form action={handleSubmit}>
<input name="comment" required />
<button type="submit">Post Comment</button>
</form>
</div>
);
}Notice how I'm using the isPending flag to visually indicate optimistic updates. This gives users feedback that their action was received, even before the server confirms it. Luckily we can style pending comments differently—maybe with reduced opacity or a subtle animation.

useOptimistic vs Manual State Management
Let me show you what this code looked like before useOptimistic. I actually had to maintain something similar in a production app:
The old way required tracking multiple state variables: actualData, pendingData, isOptimistic, and error. You'd manually merge them, handle rollbacks with useEffect, and hope you didn't miss an edge case. It was messy, error-prone, and hard to test.
With useOptimistic, React handles the complexity for you. The hook automatically manages the optimistic state, handles rollbacks when the actual state updates, and keeps everything in sync. The reducer pattern is also much easier to reason about than scattered state updates.
Production Patterns: Error Handling and Edge Cases
Here's something I learned the hard way: optimistic updates need solid error handling. You can't just assume everything will work.
First, always show error messages to users when optimistic updates fail. They clicked a button and saw instant feedback, so they need to know if something went wrong. I use toast notifications for this, but inline error messages work too.
Second, consider race conditions. What if a user clicks the like button twice rapidly? Or submits a form while a previous submission is still pending? I handle this by disabling actions during pending operations:
const [isPending, startTransition] = useTransition();
async function handleAction() {
startTransition(async () => {
addOptimistic(newValue);
await apiCall();
});
}
return <button disabled={isPending}>Submit</button>;Third, think about network failures. When your API is down, do you want to queue optimistic updates or reject them immediately? For critical actions like payments, I reject immediately. For nice-to-haves like likes, I might queue them.
Common Gotchas and Best Practices
I cannot stress this enough! The optimistic state automatically resets when the actual state updates. This is usually what you want, but it can bite you if you're not expecting it. Make sure your API response includes all the data you need to properly update the actual state.
Also, don't use useOptimistic for everything. It's perfect for user actions where feedback matters—likes, comments, favorites, toggles. But for complex forms or multi-step operations, you might need more control.
Another thing I've learned: keep your optimistic updates close to your actual state updates. If they're far apart in your component tree, you'll have a harder time keeping them in sync. Co-location is your friend here.
When to Use useOptimistic in Your App
So when should you reach for useOptimistic? I use it when:
- The action feels slow without optimistic updates (anything over 100ms)
- The success rate is high (above 95%)
- Failures can be gracefully handled with error messages
- The UI change is reversible
Don't use it when:
- Failures are common or catastrophic
- The optimistic state is complex to calculate
- Users need to see the actual result before proceeding
- You're dealing with financial transactions
In my experience, most user interactions fall into the "should use optimistic updates" category. Social features, content creation, simple toggles—these all benefit from the snappy feel that useOptimistic provides.
And that concludes the end of this post! I hope you found this valuable and look out for more in the future!