🚀 Optimistic UI: The Psychological Speed Trick
When a user clicks "Like" on a Tweet, what happens under the surface? If the app uses traditional network architecture, it takes roughly 300ms for the request (over Wi-Fi or 4G) to hit the Twitter servers, write to the database, and return a "200 OK" response to the browser.
If the user clicks "Like" and the heart icon takes a full half-second to turn red, the app feels sluggish, broken, and unpolished.
Modern enterprise web development solves this invisible delay via Optimistic UI Updates.
1️⃣ What is an Optimistic Update?
An Optimistic Update lies to the user. Instead of waiting 300ms for the server to reply, React mathematically assumes the server will reply with a success. It instantly updates the UI State in 0.001 milliseconds. The heart icon turns red the very nanosecond the user touches the screen.
The "Lie" Architecture:
- User clicks the grey heart.
- React instantly flips the state to
liked: true, ignoring reality. The heart turns red. - React fires the HTTP
POSTrequest to the server in the background. - If network fails and the server returns
500 Server Error, React catches it, rolls back the lie, flips the heart back to grey, and shows an error toast.
2️⃣ Comparing the Code
❌ Traditional Waiting (Sluggish)
function SluggishLikeButton({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [isLoading, setIsLoading] = useState(false);
const handleLike = async () => {
setIsLoading(true); // Spin the wheel
try {
// The user stares at a spinner for 300ms
await axios.post(`/api/posts/${postId}/like`);
// Only after the network finishes do we update the screen
setLikes(l => l + 1);
} catch (error) {
alert("Failed to like post.");
} finally {
setIsLoading(false);
}
};
return <button disabled={isLoading}>{isLoading ? '...' : '❤️'} {likes}</button>;
}
✅ Optimistic UI (Instantaneous)
function OptimisticLikeButton({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const handleLike = async () => {
// 1. THE OPTIMISTIC LIE! Update the UI instantly. No loading spinners!
// The user receives instant psychological feedback.
setLikes(oldLikes => oldLikes + 1);
try {
// 2. Fire the network request into the void in the background
await axios.post(`/api/posts/${postId}/like`);
// Success! The silence is golden. The UI is already correct.
} catch (error) {
// 3. DISASTER STRIKES. The Wi-Fi dropped.
console.error("Network failed.");
// 4. THE ROLLBACK. We must undo the lie to match reality.
setLikes(oldLikes => oldLikes - 1);
// Show a tiny non-intrusive toast instead of breaking the app
showToast("You lost connection. Like removed.");
}
};
return <button onClick={handleLike}>❤️ {likes}</button>;
}
3️⃣ Advanced Optimism with React Query
Building manual rollbacks for a tiny heart button is easy. But what if you are building Trello? A user drags a Kanban card from "To Do" into "Finished". The state is immensely complex (moving objects between massively nested arrays).
If you build manual rollbacks for Kanban boards, your codebase will collapse. You must use React Query (TanStack), which has a dedicated Optimistic Engine.
import { useMutation, useQueryClient } from '@tanstack/react-query';
function KanbanBoard() {
const queryClient = useQueryClient();
const mutation = useMutation({
// The Network Call
mutationFn: (newCard) => axios.post('/api/cards', newCard),
// 1. The exact millisecond the mutation fires (Before the network finishes!)
onMutate: async (newCard) => {
// Cancel any incoming re-fetches so they don't overwrite our lie
await queryClient.cancelQueries({ queryKey: ['kanban'] });
// Snapshot the previous strict reality (For the rollback)
const previousCards = queryClient.getQueryData(['kanban']);
// OPTIMISTICALLY update the massive RAM Cache instantly
queryClient.setQueryData(['kanban'], (oldData) => [...oldData, newCard]);
// Return a context object holding reality
return { previousCards };
},
// 2. If the API crashes
onError: (err, newCard, context) => {
// Restore reality from the snapshot
queryClient.setQueryData(['kanban'], context.previousCards);
showErrorToast("Card failed to save.");
},
// 3. Regardless of success of failure, force a background refresh to ensure perfect sync
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['kanban'] });
}
});
return <button onClick={() => mutation.mutate({ id: 99, title: "Do Laundry" })}>Add Card</button>;
}
4️⃣ When NOT to use Optimistic UI
Optimistic UI is powerful, but it is deeply dangerous if misused.
🚫 Never use Optimistic UI for:
- Payments / Financials. If a user clicks "Transfer $1,000", do not optimistically say "Transferred!". The user will close their laptop, but if the network drops a split-second later, they believe the money was sent when it wasn't. This causes legal nightmares.
- Irreversible/Destructive Actions. Do not optimistically say "Account Deleted Permanently!" while loading. If it fails, they are still subscribed and billing continues.
✅ Always use Optimistic UI for:
- Likes / Bookmarks / Votes. Low-stakes, highly frequent metrics.
- Drag & Drop Reordering. Waiters shouldn't stare at loading bars when re-ordering a list.
- Sending Chat Messages. iMessage and WhatsApp immediately stamp your text message on screen, while quietly placing a "Delivered" or "Sending..." indicator underneath it.
💡 Summary Lexicon
| Interaction Type | Solution | Reason |
|---|---|---|
| Critical (Payment) | Loading Spinner (Blocking) | Mathematical certainty is required before allowing the user to proceed. |
| Add/Delete (Medium Risk) | Disable Button + Spinner | Prevents the user from accidentally clicking "Delete" 5 times while network loads. |
| Likes/Votes/UI Configs | Optimistic UI | Purely psychological. The risk of sudden failure is worth the 0ms perceived speed. |