Optimistic UI Updates: Make Your App Feel Instant
Waiting for a server response before updating the UI makes apps feel slow. Optimistic updates apply the change immediately, then reconcile with the server response — reverting only if the request fails.
The Pattern
User action
↓
Update UI immediately (optimistic)
↓
Send request to server
↓
Success: confirm (no visible change needed)
Failure: revert UI + show error
With Tanstack Query
import { useMutation, useQueryClient } from '@tanstack/react-query';
function TodoItem({ todo }: { todo: Todo }) {
const queryClient = useQueryClient();
const toggleMutation = useMutation({
mutationFn: (id: string) =>
fetch(`/api/todos/${id}/toggle`, { method: 'POST' }).then(r => r.json()),
onMutate: async (id) => {
// Cancel in-flight queries to avoid overwriting optimistic update
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Snapshot previous state for rollback
const previous = queryClient.getQueryData<Todo[]>(['todos']);
// Apply optimistic update
queryClient.setQueryData<Todo[]>(['todos'], old =>
old?.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
);
return { previous };
},
onError: (err, id, context) => {
// Revert on error
queryClient.setQueryData(['todos'], context?.previous);
},
onSettled: () => {
// Always refetch to sync with server
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<div
onClick={() => toggleMutation.mutate(todo.id)}
className={todo.completed ? 'line-through opacity-50' : ''}
>
{todo.title}
</div>
);
}
With React useState (Simple Cases)
function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes);
const [liked, setLiked] = useState(false);
async function handleLike() {
// Update immediately
setLiked(true);
setLikes(l => l + 1);
try {
await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
} catch {
// Revert on failure
setLiked(false);
setLikes(l => l - 1);
}
}
return (
<button onClick={handleLike} disabled={liked}>
{liked ? '❤️' : '🤍'} {likes}
</button>
);
}
Next.js Server Action Optimism
'use client';
import { useOptimistic } from 'react';
function MessageList({ messages, sendMessage }: Props) {
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(state, newMessage: string) => [
...state,
{ id: 'temp', text: newMessage, pending: true }
]
);
async function handleSubmit(formData: FormData) {
const text = formData.get('text') as string;
addOptimistic(text); // Show immediately
await sendMessage(formData); // Then persist
}
return (
<>
{optimisticMessages.map(m => (
<div key={m.id} className={m.pending ? 'opacity-50' : ''}>
{m.text}
</div>
))}
<form action={handleSubmit}>
<input name='text' />
<button type='submit'>Send</button>
</form>
</>
);
}
When NOT to Use Optimistic Updates
- Financial transactions (always confirm server-side first)
- Irreversible actions (deletion — show confirmation instead)
- Actions where server response contains new data needed immediately
Optimistic UI patterns are implemented throughout the dashboard in the AI SaaS Starter Kit. $99 at whoffagents.com.
Top comments (0)