patterntypescriptreactModerate
TanStack Query — optimistic updates with rollback on error
Viewed 0 times
optimistic updateonMutateonError rollbackcancelQueriesgetQueryDatasetQueryDatacontext snapshot
Problem
After a mutation the UI waits for the server response before reflecting changes. For fast actions (like toggling a like button), this creates noticeable lag. Optimistic updates apply the change immediately and roll back if the server rejects it.
Solution
Use onMutate to apply optimistic state, onError to roll back, and onSettled to refetch:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function LikeButton({ postId }: { postId: number }) {
const queryClient = useQueryClient();
const likeMutation = useMutation({
mutationFn: (id: number) =>
fetch(
onMutate: async (id) => {
// Cancel in-flight queries for this post
await queryClient.cancelQueries({ queryKey: ['posts', id] });
// Snapshot the previous value
const previous = queryClient.getQueryData<Post>(['posts', id]);
// Optimistically update
queryClient.setQueryData<Post>(['posts', id], (old) =>
old ? { ...old, likes: old.likes + 1 } : old
);
return { previous };
},
onError: (_err, id, context) => {
// Roll back to the snapshot
if (context?.previous) {
queryClient.setQueryData(['posts', id], context.previous);
}
},
onSettled: (_data, _err, id) => {
// Always refetch to sync with server truth
queryClient.invalidateQueries({ queryKey: ['posts', id] });
},
});
return <button onClick={() => likeMutation.mutate(postId)}>Like</button>;
}
import { useMutation, useQueryClient } from '@tanstack/react-query';
function LikeButton({ postId }: { postId: number }) {
const queryClient = useQueryClient();
const likeMutation = useMutation({
mutationFn: (id: number) =>
fetch(
/api/posts/${id}/like, { method: 'POST' }).then((r) => r.json()),onMutate: async (id) => {
// Cancel in-flight queries for this post
await queryClient.cancelQueries({ queryKey: ['posts', id] });
// Snapshot the previous value
const previous = queryClient.getQueryData<Post>(['posts', id]);
// Optimistically update
queryClient.setQueryData<Post>(['posts', id], (old) =>
old ? { ...old, likes: old.likes + 1 } : old
);
return { previous };
},
onError: (_err, id, context) => {
// Roll back to the snapshot
if (context?.previous) {
queryClient.setQueryData(['posts', id], context.previous);
}
},
onSettled: (_data, _err, id) => {
// Always refetch to sync with server truth
queryClient.invalidateQueries({ queryKey: ['posts', id] });
},
});
return <button onClick={() => likeMutation.mutate(postId)}>Like</button>;
}
Why
cancelQueries prevents an in-flight GET from overwriting the optimistic update. The context object returned from onMutate is passed to onError and onSettled, enabling rollback with the snapshotted value.
Gotchas
- Always call cancelQueries before the optimistic update — otherwise an in-flight response can overwrite it
- The context return value from onMutate is typed as unknown by default — provide a generic to useMutation for inference
- onSettled runs after both onSuccess and onError — it is the right place for final cache sync
- Optimistic updates can confuse users if the rollback is jarring — consider only using this for simple boolean toggles
Revisions (0)
No revisions yet.