Learn how to implement Optimistic UI patterns in React. Master optimistic rollback logic, server-client synchronization, and failure handling for elite UX.
Previously in this course, we explored Mastering Suspense for Data Fetching: A Declarative Approach to handle loading states. While loading skeletons improve perceived performance, they don't solve the "latency gap" in user interactions. Today, we bridge that gap with Optimistic UI.
Optimistic UI is the practice of updating the application state immediately upon user interaction, assuming the server will succeed, while keeping the "real" state in the background. It transforms the user experience from "click, wait, see result" to "click, see result instantly."
At its core, an optimistic update follows a three-step lifecycle:
We typically implement this using a mutation library like React Query (TanStack Query), which simplifies the orchestration of these steps. If you haven't mastered the basics yet, review Mastering Mutations and Data Updates with React Query.
Here is a concrete example of an optimistic "Like" button in our project:
TSXconst useLikePost = (postId: string) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: (newLikeStatus: boolean) => api.updateLike(postId, newLikeStatus), // 1. Snapshot: Cancel outgoing fetches so they don't overwrite our optimistic update onMutate: async (newLikeStatus) => { await queryClient.cancelQueries({ queryKey: [CE9178">'post', postId] }); const previousPost = queryClient.getQueryData([CE9178">'post', postId]); // 2. Optimistic Update: Manually update the cache queryClient.setQueryData([CE9178">'post', postId], (old: any) => ({ ...old, liked: newLikeStatus, })); // Return context for rollback return { previousPost }; }, // 3. Failure Handling: Roll back on error onError: (err, newLikeStatus, context) => { queryClient.setQueryData([CE9178">'post', postId], context.previousPost); }, // Always refetch after error or success to ensure synchronization onSettled: () => { queryClient.invalidateQueries({ queryKey: [CE9178">'post', postId] }); }, }); };
The most common mistake is failing to handle the "rollback" or the "final state." If you update the UI optimistically but the server returns an error, failing to revert the state leaves the user in an inconsistent, confusing reality.
When the server request fails, you must revert the UI to the exact state it was in before the interaction. The context object returned in onMutate is your safety net. Always store the previous state there.
| Action | UI State | Server State |
|---|---|---|
| Initial | Synced | Synced |
| Mutation | Optimistic | Pending |
| Success | Synced | Synced |
| Failure | Rollback | Reverted |
In our project, we have a list of task items. Currently, toggling a task completion shows a loading spinner for 500ms.
Your task:
toggleTask mutation to be optimistic.queryClient.cancelQueries to prevent the "flicker" caused by a late-arriving background fetch.onError to inform the user that the change could not be saved, effectively communicating the rollback.queryClient.cancelQueries before performing the optimistic update.setQueryData logic is immutable. Don't just update the one field; ensure the entire object structure is maintained correctly.Optimistic UI is a powerful tool for perceived performance, but it requires rigorous state management. By capturing snapshots, performing atomic cache updates, and implementing robust error rollback handlers, you can create interfaces that feel instantaneous while maintaining absolute data integrity.
Up next, we will tackle Advanced Cache Invalidation, where we'll learn how to keep fragmented data stores in sync across complex component trees.
Stop managing manual boolean loading flags. Learn to implement Suspense boundaries to orchestrate data fetching and create seamless, declarative loading states.
Read moreLearn how to manage large datasets in React using pagination, infinite scrolling, and virtualization to maintain high performance and a smooth user experience.
Optimistic UI Updates
Final Project Audit & Optimization
Advanced Hook Patterns
Managing Global State with Zustand/Redux
Testing Performance-Critical Components
Static Site Generation (SSG) Patterns
Internationalization (i18n) Architecture
Accessibility (a11y) in Advanced Components
Managing Third-Party Integrations
Advanced Form Handling
Using Portals for UI Overlays
Implementing Virtualized Lists
Building Design System Primitives
Managing Large-Scale Data Fetching
Micro-Frontends with React
Security Best Practices in React
Advanced Ref Usage
Memoization Pitfalls
Mastering React Patterns for Scalability
Advanced TypeScript with React