Optimistic Updates: Building Instant, Responsive UIs
December 20, 2025
When you tap "Like" on a post and it turns blue instantly, you just experienced an optimistic update.
Instead of waiting for the server to confirm that your action succeeded, the UI assumes success, updates immediately, and quietly fixes things in the background if something goes wrong. Done well, this makes your app feel instant and delightful—even on slow networks.
In this post, we'll explore what optimistic updates are, when to use them, how to implement them in React (including React Query and React 19's useOptimistic), and how to avoid the most common pitfalls.
What Are Optimistic Updates?
Optimistic updates are a UI pattern where you:
- Update the UI immediately in response to a user action.
- Send the request to the server in the background.
- Rollback or adjust the UI if the server responds with an error.
Instead of:
- user clicks → wait for response → then update UI
You do:
- user clicks → update UI immediately → wait for response → keep or rollback
Key Characteristics
- Immediate feedback: The UI reacts instantly to user input.
- Assumes success: You design for the common case where the request succeeds.
- Rollback mechanism: You keep enough information to restore the previous state if needed.
This pattern is especially powerful for list-based UIs, toggles, and social interactions, where a short delay feels worse than a rare correction.
Why Optimistic Updates Matter
User Experience Benefits
- Perceived performance: Users feel like your app is fast, even if the network isn't.
- Reduced latency pain: Actions feel local instead of "remote".
- App-like feel: Web apps start to feel like native apps.
Business & Product Impact
- Higher engagement: Users are more willing to interact when the UI feels snappy.
- Lower abandonment: Less waiting means fewer "did it even work?" moments.
- Better first impressions: Perceived speed is often more important than raw benchmarks.
Technical Benefits
- Hides network variance: UI remains smooth even when the backend isn't.
- Pairs well with caching: Optimistic updates on top of a good cache make apps feel real-time.
When to Use (and Not Use) Optimistic Updates
Great Use Cases
- Social interactions: likes, bookmarks, follows, upvotes.
- Simple toggles: feature flags, preferences, favorite buttons.
- CRUD on non-critical data:
- Creating / editing / deleting todos, notes, comments.
- Reordering items in a list.
- Local-first UX: Data is primarily manipulated on the client and synced later.
Be Careful or Avoid When
- Money is involved: payments, refunds, wallet balances.
- Irreversible or destructive actions: permanent deletions, security-sensitive changes.
- Heavy validation is required: operations that often fail server-side.
- Conflict-prone data: complex shared documents without conflict resolution.
Quick Decision Checklist
Ask yourself:
- "If this fails, can I safely rollback without confusing the user?"
- "Is this operation highly likely to succeed?"
- "Would an occasional undo feel natural and acceptable to users?"
If the answers are mostly "yes", optimistic updates are a strong candidate.
Basic Implementation Pattern (React useState)
Let's start with a minimal optimistic "favorite" toggle.
import { useState } from "react";
async function fakeToggleFavoriteApi(nextValue: boolean) {
// Simulate network latency
await new Promise((resolve) => setTimeout(resolve, 800));
// Simulate occasional failure
if (Math.random() < 0.2) {
throw new Error("Failed to update favorite. Please try again.");
}
return nextValue;
}
export function BasicOptimisticFavorite() {
const [isFavorite, setIsFavorite] = useState(false);
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleToggle() {
if (isPending) return;
setError(null);
// 1. Snapshot previous value
const previous = isFavorite;
// 2. Optimistically update UI
const next = !isFavorite;
setIsFavorite(next);
setIsPending(true);
try {
// 3. Fire request in background
await fakeToggleFavoriteApi(next);
} catch (err) {
// 4. Rollback on failure
setIsFavorite(previous);
setError((err as Error).message);
} finally {
setIsPending(false);
}
}
return (
<div className="inline-flex flex-col items-start gap-2 text-sm">
<button
onClick={handleToggle}
disabled={isPending}
className={`rounded-full border px-3 py-1 text-xs md:text-sm transition-colors ${
isFavorite
? "bg-green-600 text-white border-green-700"
: "bg-background text-foreground hover:bg-muted"
}`}
>
{isFavorite ? "★ Favorited" : "☆ Add to favorites"}
{isPending && <span className="ml-2 opacity-80">Saving…</span>}
</button>
{error && <p className="text-xs text-red-500">{error}</p>}
</div>
);
}This example illustrates the core optimistic pattern:
- Snapshot the previous value.
- Update state immediately to the next value.
- Run the async request in the background.
- Rollback if the request fails.
We'll reuse this mental model in the rest of the post.
Try It: Optimistic Like Button
Click the button to see optimistic updates in action. Notice how it updates instantly, then syncs in the background. Occasionally it will fail to demonstrate rollback.
Implementation Patterns
1. React Query / TanStack Query
React Query has first-class support for optimistic updates via useMutation.
import { useMutation, useQueryClient } from "@tanstack/react-query";
function useToggleTodo(id: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
// Call your API here
},
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: ["todos"] });
// Snapshot previous value
const previousTodos = queryClient.getQueryData<Todo[]>(["todos"]);
// Optimistically update cache
queryClient.setQueryData<Todo[]>(["todos"], (old = []) =>
old.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
// Return context for rollback
return { previousTodos };
},
onError: (_error, _variables, context) => {
// Rollback to snapshot
if (context?.previousTodos) {
queryClient.setQueryData(["todos"], context.previousTodos);
}
},
onSettled: () => {
// Refetch to ensure server and client are in sync
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
}This pattern is great when:
- Your data already lives in React Query.
- You want automatic cache updates and refetch on settle.
- You prefer colocating optimistic logic with mutations.
Try It: Optimistic Todo List
Add, toggle, and delete todos to see optimistic updates in action. Items appear instantly, then sync in the background. Try adding multiple items quickly to see how it handles rapid updates.
- Learn optimistic updates
- Build a todo app
2. Custom "Optimistic Mutation" Hook
For non-React-Query setups, you can encapsulate the snapshot + optimistic update + rollback pattern in a custom hook.
import { useState } from "react";
type OptimisticConfig<T> = {
getOptimisticValue: (current: T, input: T) => T;
perform: (value: T) => Promise<void>;
};
export function useOptimisticValue<T>(initial: T, config: OptimisticConfig<T>) {
const [value, setValue] = useState<T>(initial);
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<string | null>(null);
async function update(nextInput: T) {
if (isPending) return;
setError(null);
const previous = value;
const optimistic = config.getOptimisticValue(value, nextInput);
setValue(optimistic);
setIsPending(true);
try {
await config.perform(optimistic);
} catch (err) {
setValue(previous);
setError((err as Error).message);
} finally {
setIsPending(false);
}
}
return { value, update, isPending, error } as const;
}You can then reuse this hook for favorites, counters, toggles, and more by passing different getOptimisticValue and perform functions.
3. State Management Libraries
The same optimistic ideas apply in Zustand, Redux Toolkit, or Context:
- Zustand: perform an optimistic
setin the store, and rollback on error using a snapshot. - Redux Toolkit / RTK Query: use extra reducers or RTK Query's update utilities for optimistic cache updates.
- Context: keep shared state in context and expose helper methods that implement the optimistic pattern.
4. React 19's useOptimistic Hook
React 19 introduced a dedicated hook for optimistic state: useOptimistic.
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);state: the "real" state value.updateFn(currentState, optimisticValue): a pure function that returns the optimistic state.optimisticState: equalsstateunless an update is pending.addOptimistic(optimisticValue): triggers an optimistic update by callingupdateFn.
A classic example is a message thread: when the user sends a message, you want it to appear immediately with a "Sending…" label.
import { useOptimistic, useState, useTransition } from "react";
function deliverMessage(message: string) {
return new Promise<string>((resolve) => {
setTimeout(() => resolve(message), 800);
});
}
export function UseOptimisticThread() {
const [messages, setMessages] = useState<
{ text: string; sending?: boolean }[]
>([{ text: "Hello there!", sending: false }]);
const [isPending, startTransition] = useTransition();
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage: string) => [
{ text: newMessage, sending: true },
...state,
]
);
const formRef = useRef<HTMLFormElement>(null);
async function handleSend(formData: FormData) {
const text = String(formData.get("message") || "").trim();
if (!text) return;
// Makes sure the input is cleared after the message is sent instantly, instead of waiting for the server response
formRef.current?.reset();
addOptimisticMessage(text);
startTransition(async () => {
const delivered = await deliverMessage(text);
setMessages((current) => [
{ text: delivered, sending: false },
...current,
]);
});
}
return (
<div className="space-y-3 text-sm">
<form
ref={formRef}
action={handleSend}
className="flex items-center gap-2"
>
<input
name="message"
placeholder="Say hi!"
className="flex-1 rounded-md border bg-background px-2 py-1 text-xs md:text-sm"
/>
<button
type="submit"
disabled={isPending}
className="rounded-md bg-primary px-3 py-1 text-xs md:text-sm text-primary-foreground disabled:opacity-60"
>
Send
</button>
</form>
<div className="space-y-1">
{optimisticMessages.map((message, index) => (
<div key={index} className="text-xs md:text-sm">
{message.text}
{message.sending && (
<span className="ml-1 text-[10px] text-muted-foreground">
(Sending…)
</span>
)}
</div>
))}
</div>
</div>
);
}useOptimistic shines when you already use React Server Components or Server Actions and want optimistic UI without manually managing snapshots.
Try It: Message Thread with useOptimistic
Send messages to see React 19's useOptimistic hook in action. Messages appear instantly with a 'Sending...' indicator, then sync in the background.
Error Handling Strategies
Optimistic updates are only safe if you handle errors well.
1. Rollback Mechanisms
- Snapshot pattern: store the previous state before the optimistic change, then restore it on error.
- Inverse operation: sometimes you can compute a rollback from the current state (e.g., toggling a boolean twice).
- Automatic rollback with retry: roll back automatically on error, show a clear error message, and provide a retry button.
Note: "Undo UI" (letting users explicitly undo) is better suited for successful operations where users might want to reverse their action (like Gmail's "undo send"). For failed optimistic updates, automatic rollback with clear error messaging is typically better UX than asking users to undo something that never actually happened.
2. Communicating Errors
- Toasts for transient issues ("Failed to like. Tap to retry").
- Inline messaging near the component when the context matters.
- Retry buttons when the action is important but not critical.
3. Edge Cases to Consider
- Network failures: offline, timeouts, flaky connections.
- Server validation errors: the server rejects invalid or stale data.
- Race conditions: multiple updates to the same entity at once.
- Cross-device conflicts: another device updates the same record.
Always ask: what does the user see if this fails? and can they recover?
4. Subtle Bugs and Inconsistencies
Optimistic updates can introduce subtle bugs that are easy to miss:
-
State drift: The client shows one state, but the server has a different state. This can happen if the server modifies your data (e.g., normalizing text, adding timestamps, or applying business rules) and you don't reconcile after the response.
-
Stale optimistic state: If a user navigates away or the component unmounts before the request completes, you might have orphaned optimistic updates that never get reconciled.
-
Ordering issues: Rapid clicks can cause optimistic updates to apply in a different order than server responses arrive. For example, clicking "like" then "unlike" quickly might result in both requests succeeding, leaving the final state incorrect.
-
Server response mismatch: The server might return data that differs from your optimistic update (e.g., different ID, modified timestamp, or transformed values). If you don't handle this, your UI can show stale or incorrect data.
-
Memory leaks: Pending optimistic updates that never complete can accumulate, especially if you're tracking them in arrays or maps without cleanup.
Mitigation strategies:
- Always reconcile with server responses (use
onSettledin React Query, or refetch after mutations). - Cancel pending requests when components unmount.
- Use request IDs or timestamps to handle out-of-order responses.
- Validate that server responses match your expectations before applying them.
Real-World Examples
Social "Like" Button
- Optimistically toggle the like state and count.
- Rollback if the request fails.
- Optionally show a subtle error or toast.
The like button demo above demonstrates this pattern in action.
Todo Application
- Optimistically add a new todo to the list.
- Instantly strike-through when marking complete.
- Optimistically delete while providing an undo for a few seconds.
The todo list demo above shows a complete CRUD implementation with optimistic updates.
Shopping Cart
- Optimistically add items to the cart badge.
- Update quantities immediately while syncing in the background.
- If a product goes out of stock, roll back and explain why.
Side-by-Side Comparison
See the difference between optimistic and pessimistic updates:
Optimistic vs Pessimistic Updates
Click both buttons to see the difference. The optimistic version updates instantly, while the pessimistic version waits for the server response.
With Optimistic Update
UI updates instantly, syncs in background
Without Optimistic Update
UI waits for server response
Best Practices & Common Pitfalls
Do
- Always keep enough information to rollback.
- Design for failure: test with the network throttled and forced errors.
- Show intent: small "Saving…" or "Syncing…" labels can build trust. However, for non-mission-critical operations or actions that succeed close to 100% of the time (like likes or simple toggles), you may not need to show unnecessary labels—the instant UI update is often feedback enough.
- Refetch or reconcile after the server responds to avoid long-term drift.
Don't
- Don't use optimistic updates for irreversible, high-risk actions.
- Don't hide errors completely—users should understand when something failed.
- Don't ignore race conditions; think about ordering and last-write-wins.
Avoiding Over-Engineering
It's easy to over-engineer optimistic updates. Here's how to keep it simple:
-
Start simple: For basic toggles or counters, a simple
useStatewith snapshot rollback is often enough. You don't need React Query or complex state machines. -
Don't optimize prematurely: If your operation succeeds 99%+ of the time and failures are rare, a basic rollback might be sufficient. You don't need elaborate retry queues, exponential backoff, or conflict resolution for a like button.
-
Use existing tools: React Query's
useMutationand React 19'suseOptimisticalready handle many edge cases. Don't reinvent the wheel unless you have specific requirements. -
Progressive enhancement: Start with optimistic updates for the happy path, then add error handling, retries, and reconciliation only when you encounter real issues.
-
Know when to skip: If implementing optimistic updates requires complex state synchronization, conflict resolution, or extensive testing, consider whether the UX benefit is worth the added complexity. Sometimes waiting for the server response is fine.
Remember: the goal is to make your UI feel fast, not to build a perfect distributed system. Keep it simple, test the failure cases, and iterate based on real user feedback.
Performance Considerations
Optimistic updates help perceived performance, but you should still:
- Avoid unnecessary re-renders (update minimal state, memoize where needed).
- Batch updates when possible.
- Measure using real devices and slow networks.
Wrapping Up
Optimistic updates are one of the most effective ways to make your UI feel fast and alive:
- You assume success for common paths.
- You handle failure gracefully with rollbacks and clear messaging.
- You can implement them with plain React state, React Query, or React 19's
useOptimistic.
Start small: pick a low-risk interaction like a like button or todo toggle, add an optimistic update, and observe how much better the app feels. Then extend the pattern to more flows where speed and responsiveness matter most.
Additional Resources
Official Documentation
- React
useOptimisticHook - Official React documentation for theuseOptimistichook - TanStack Query Optimistic Updates - Comprehensive guide to optimistic updates with React Query
- React Server Actions - Learn how to combine
useOptimisticwith Server Actions
Further Reading
- UX Patterns: Research on perceived performance and user experience patterns
- Performance Optimization: Techniques for measuring and improving perceived performance
- State Management: Best practices for managing complex state in React applications
Related Blog Posts
If you're interested in building more interactive content, check out my other posts on building an MDX blog and adding interactive demos to MDX.
Practice
Try implementing optimistic updates in your own projects:
- Start with a simple toggle (like/favorite button)
- Move to list operations (add/remove items)
- Experiment with React Query or
useOptimisticfor more complex scenarios - Test failure scenarios to ensure your rollback logic works correctly
Remember: the goal is to make your UI feel instant and responsive, not to hide errors. Always provide clear feedback when things go wrong.