- Published on
React 19: Mastering the useOptimistic Hook for Better UX
- Authors

- Name
- Mehdi Akiki
React 19: Mastering the useOptimistic Hook for Better UX
React 19 introduces several groundbreaking features that enhance developer experience and application performance. Among these, the useOptimistic hook stands out as a powerful tool for creating responsive user interfaces. This hook enables optimistic UI updates, providing instant feedback to users while handling the underlying data synchronization seamlessly.
What is Optimistic UI?
Optimistic UI is a design pattern where the interface immediately reflects user actions before the server confirms them. This creates a perception of speed and responsiveness, even when network requests are involved.
Traditional Approach vs Optimistic UI
Traditional Approach:
const [likes, setLikes] = useState(0);
const [loading, setLoading] = useState(false);
const handleLike = async () => {
setLoading(true);
try {
await api.likePost(postId);
setLikes((prev) => prev + 1);
} catch (error) {
// Handle error
} finally {
setLoading(false);
}
};
With Optimistic UI: The UI updates immediately, then reconciles with server response.
Introducing useOptimistic Hook
The useOptimistic hook in React 19 simplifies optimistic updates by managing the state transitions automatically. It takes an initial state and a reducer function, similar to useReducer.
Basic Syntax
const [optimisticState, addOptimistic] = useOptimistic(
initialState,
(currentState, optimisticValue) => {
// Return the new optimistic state
return { ...currentState, ...optimisticValue };
}
);
Practical Examples
Like Button with Optimistic Updates
import { useOptimistic } from "react";
function LikeButton({ postId, initialLikes }) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(currentLikes, newLike) => currentLikes + 1
);
const handleLike = async () => {
// Optimistically update the UI
addOptimisticLike(1);
try {
await api.likePost(postId);
} catch (error) {
// Revert the optimistic update on error
addOptimisticLike(-1);
}
};
return <button onClick={handleLike}>❤️ {optimisticLikes} likes</button>;
}
Shopping Cart with Optimistic Updates
function ShoppingCart({ items, onUpdateCart }) {
const [optimisticCart, addOptimisticItem] = useOptimistic(items, (currentCart, newItem) => [
...currentCart,
newItem,
]);
const addToCart = async (product) => {
// Optimistically add item to cart
addOptimisticItem(product);
try {
await api.addToCart(product.id);
onUpdateCart(); // Refresh actual cart data
} catch (error) {
// Remove the optimistic item on error
addOptimisticItem(product, true); // Remove mode
}
};
return (
<div>
<h2>Cart ({optimisticCart.length} items)</h2>
{optimisticCart.map((item, index) => (
<CartItem key={index} item={item} />
))}
</div>
);
}
Form Submission with Optimistic Updates
function CommentForm({ postId, onCommentAdded }) {
const [optimisticComments, addOptimisticComment] = useOptimistic(
[],
(currentComments, newComment) => [...currentComments, newComment]
);
const handleSubmit = async (formData) => {
const comment = {
id: Date.now(), // Temporary ID
text: formData.get("comment"),
author: "You",
timestamp: new Date().toISOString(),
pending: true, // Mark as pending
};
// Add optimistic comment
addOptimisticComment(comment);
try {
const savedComment = await api.addComment(postId, comment);
onCommentAdded(savedComment);
} catch (error) {
// Remove the optimistic comment
addOptimisticComment(comment, true);
}
};
return (
<form action={handleSubmit}>
<textarea name="comment" placeholder="Add a comment..." />
<button type="submit">Post Comment</button>
{/* Display optimistic comments */}
{optimisticComments.map((comment) => (
<div key={comment.id} className={comment.pending ? "opacity-50" : ""}>
<strong>{comment.author}</strong>: {comment.text}
{comment.pending && <span>(sending...)</span>}
</div>
))}
</form>
);
}
Advanced Patterns
Handling Multiple Optimistic Updates
function TaskList({ tasks, onUpdateTask }) {
const [optimisticTasks, updateOptimisticTask] = useOptimistic(
tasks,
(currentTasks, { id, updates }) => {
return currentTasks.map((task) => (task.id === id ? { ...task, ...updates } : task));
}
);
const toggleTask = async (taskId) => {
const task = optimisticTasks.find((t) => t.id === taskId);
const newStatus = !task.completed;
// Optimistically update
updateOptimisticTask({ id: taskId, updates: { completed: newStatus } });
try {
await api.updateTask(taskId, { completed: newStatus });
} catch (error) {
// Revert on error
updateOptimisticTask({ id: taskId, updates: { completed: !newStatus } });
}
};
return (
<ul>
{optimisticTasks.map((task) => (
<li
key={task.id}
onClick={() => toggleTask(task.id)}
className={task.completed ? "line-through" : ""}
>
{task.title}
</li>
))}
</ul>
);
}
Optimistic Updates with Server State Management
import { useOptimistic } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
function PostList() {
const { data: posts } = useQuery(["posts"], fetchPosts);
const [optimisticPosts, addOptimisticPost] = useOptimistic(
posts || [],
(currentPosts, newPost) => [newPost, ...currentPosts]
);
const createPostMutation = useMutation({
mutationFn: createPost,
onSuccess: (newPost) => {
// Server response received, update with real data
queryClient.setQueryData(["posts"], (old) => [newPost, ...old]);
},
onError: () => {
// Remove optimistic post on error
addOptimisticPost(null, true);
},
});
const handleCreatePost = (postData) => {
const optimisticPost = {
id: `temp-${Date.now()}`,
...postData,
createdAt: new Date().toISOString(),
optimistic: true,
};
addOptimisticPost(optimisticPost);
createPostMutation.mutate(postData);
};
return (
<div>
{optimisticPosts.map((post) => (
<Post key={post.id} post={post} isOptimistic={post.optimistic} />
))}
</div>
);
}
Best Practices
1. Provide Visual Feedback
Always indicate when an action is optimistic:
function OptimisticButton({ children, onClick, isOptimistic }) {
return (
<button
onClick={onClick}
disabled={isOptimistic}
className={isOptimistic ? "cursor-not-allowed opacity-50" : ""}
>
{isOptimistic ? "Saving..." : children}
</button>
);
}
2. Handle Errors Gracefully
const [optimisticState, addOptimistic] = useOptimistic(
initialState,
(currentState, { action, payload, revert }) => {
if (revert) {
// Revert to previous state
return currentState.previousState;
}
// Store current state for potential revert
const newState = { ...currentState, ...payload, previousState: currentState };
return newState;
}
);
3. Use Appropriate Timing
// For fast actions (likes, favorites)
const [optimisticLikes, addLike] = useOptimistic(likes, increment);
// For slower actions (form submissions)
const [optimisticData, updateData] = useOptimistic(
data,
(current, update) => ({ ...current, ...update }),
{ timeout: 3000 } // Custom timeout
);
4. Combine with Error Boundaries
class OptimisticErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log error and potentially revert optimistic updates
this.props.onError?.(error);
}
render() {
if (this.state.hasError) {
return <div>Something went wrong. Please refresh the page.</div>;
}
return this.props.children;
}
}
Performance Considerations
Memory Management
// Clean up optimistic updates after server confirmation
useEffect(() => {
const cleanup = setTimeout(
() => {
// Remove temporary optimistic items after 5 minutes
setOptimisticItems((items) => items.filter((item) => !item.temp));
},
5 * 60 * 1000
);
return () => clearTimeout(cleanup);
}, [optimisticItems]);
Debouncing Optimistic Updates
function useDebouncedOptimistic(value, delay = 300) {
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
useEffect(() => {
const timer = setTimeout(() => {
setOptimisticValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return optimisticValue;
}
Migration from React 18
Before (React 18)
const [state, setState] = useState(initialState);
const [optimisticState, setOptimisticState] = useState(initialState);
const updateOptimistic = (update) => {
setOptimisticState(update);
// Manual server sync and error handling
};
After (React 19)
const [optimisticState, updateOptimistic] = useOptimistic(initialState, (currentState, update) => ({
...currentState,
...update,
}));
// Automatic state management with built-in error handling
Real-World Use Cases
1. Social Media Interactions
- Like/unlike posts
- Follow/unfollow users
- Bookmark content
- Share posts
2. E-commerce
- Add to cart
- Update quantities
- Apply coupons
- Wishlist management
3. Collaborative Editing
- Real-time document updates
- Comment threads
- Task assignments
- Status changes
4. Form Submissions
- Contact forms
- Registration forms
- Settings updates
- Profile changes
Testing Optimistic Updates
// Test optimistic updates
test("optimistic like updates immediately", () => {
const { result } = renderHook(() => useOptimistic(0, increment));
act(() => {
result.current[1](1); // addOptimistic
});
expect(result.current[0]).toBe(1); // optimisticState
});
// Test error handling
test("reverts optimistic update on error", async () => {
const mockApi = jest.fn().mockRejectedValue(new Error("API Error"));
// Test implementation
});
Conclusion
The useOptimistic hook in React 19 represents a significant improvement in building responsive user interfaces. By providing instant feedback and handling state reconciliation automatically, it enables developers to create applications that feel fast and responsive, even with network latency.
Key benefits include:
- Better UX: Instant visual feedback
- Simplified Code: Less boilerplate for optimistic updates
- Automatic Reconciliation: Built-in error handling and state management
- Performance: Reduced perceived latency
As you migrate to React 19, consider using useOptimistic for any user interactions that would benefit from immediate feedback. The hook works seamlessly with existing React patterns and can significantly improve the perceived performance of your applications.
Remember to always provide clear visual feedback for optimistic updates and handle errors gracefully to maintain user trust. With proper implementation, optimistic UI can make your applications feel lightning-fast while maintaining data consistency.
I build and scale reliable production systems. Open to full-time and freelance work with U.S.-based teams that value ownership and execution.
Got something in mind?
Book a Discovery Call