React cookbook · Intermediate
Add optimistic updates to a todo list
Make toggling and deleting items feel instant while staying correct if the server request fails.
Approach
With TanStack Query, update the cached list immediately in onMutate, keep a snapshot to roll back in onError, and invalidate in onSettled to sync with the server. Cancel in-flight queries first so they don't overwrite the optimistic state.
Solution
import { useMutation, useQueryClient } from "@tanstack/react-query";
export function useDeleteTodo() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const res = await fetch(`/api/todos/${id}`, { method: "DELETE" });
if (!res.ok && res.status !== 404) throw new Error("Delete failed");
},
onMutate: async (id) => {
await qc.cancelQueries({ queryKey: ["todos"] });
const previous = qc.getQueryData<Todo[]>(["todos"]);
qc.setQueryData<Todo[]>(["todos"], (old = []) => old.filter((t) => t.id !== id));
return { previous };
},
onError: (_err, _id, ctx) => {
qc.setQueryData(["todos"], ctx?.previous);
toast.error("Couldn't delete the item");
},
onSettled: () => qc.invalidateQueries({ queryKey: ["todos"] }),
});
}
// <button onClick={() => deleteTodo.mutate(todo.id)} aria-label={`Delete ${todo.title}`}>Delete</button>
Pitfalls
- Forgetting the rollback, leaving the UI lying after failures.
- Treating 404 on DELETE as an error (it's already gone — idempotent).
- Optimistic updates for operations that commonly fail validation.
Variations
- React 19
useOptimisticfor form Actions. - Show a pending style (
opacity) instead of removing immediately.
Further reading: TanStack Query: Optimistic updates
Reference: https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates
More React recipes
- Build an accessible modal dialog
- Build infinite scroll with TanStack Query
- Call a cookie-authenticated API with CSRF protection
- Debounce a search input in React
- Handle errors gracefully with error boundaries
- Implement dark mode that follows the system
- Poll a long-running job's status
- Protect routes in a React SPA