useMutation and Optimistic Updates: Don't Let the User Feel Even 0.1s of Delay

Let's try clicking the 'Like' button in the SNS app Chris built.
When clicked, a loading spinner spins for 0.5 seconds, and then the heart turns red. Functionally, it's perfect. But it lacks that "snappy" feel you get when using Instagram or Twitter.
Users might tolerate their own slow internet, but they cannot tolerate a slow app response. Waiting for the server to respond is too honest and slow.
Today, we will implement Optimistic Updates, a bold technique where we assume success and update the screen immediately, even before the server responds.
1. Honest Updates vs. Optimistic Updates
The typical mutation flow looks like this:
The 0.2~0.5 second gap here determines the User Experience (UX).
Optimistic Updates flip this order:
2. Implementing with TanStack Query
TanStack Query's useMutation provides the perfect Lifecycle Hooks for these optimistic updates.
The Core 3-Step Logic
Implementation in Code
Let's look at the 'Like' feature Chris wrote.
// useLikeTodo.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useLikeTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (todoId: string) => api.likeTodo(todoId),
// 1. Runs the moment the mutation starts
onMutate: async (todoId) => {
// Cancel ongoing queries to prevent overwrites
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Save previous state for rollback (Snapshot)
const previousTodos = queryClient.getQueryData(['todos']);
// Forcefully modify the cache (Don't wait for server response!)
queryClient.setQueryData(['todos'], (old: Todo[]) => {
return old.map(todo =>
todo.id === todoId ? { ...todo, liked: true } : todo
);
});
// Return snapshot (Used in onError)
return { previousTodos };
},
// 2. Runs if an error occurs
onError: (err, newTodo, context) => {
// Rollback to snapshot
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
alert("Failed to like! Please try again later.");
},
// 3. Runs when finished, regardless of success or failure
onSettled: () => {
// Refetch to ensure data consistency with server
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
}3. Handling UX Details
Optimistic updates are powerful, but they shouldn't be abused.
When to use?
When NOT to use?
Handling Failure Scenarios
If an optimistic update fails, the user gets confused because the red heart suddenly turns empty again. Therefore, it is essential to kindly inform them via a Toast message in onError saying, "The action was not applied."
Key Takeaways
Now that we've gifted the user a snappy experience, we need to think about how to handle massive amounts of data. How do we implement a feed where you scroll endlessly?
Continuing in: “useInfiniteQuery and Intersection Observer: The Standard for Infinite Scroll”