Nếu không huỷ, một refetch đang bay (bắt đầu trước khi mutation chạy) có thể trả về sau khi bạn đã ghi cache lạc quan và ghi đè giá trị mới bằng dữ liệu cũ của server — UI nhảy về trạng thái trước đó rồi mới đúng lại sau. cancelQueries chặn đúng trường hợp này.
Bốn bước chuẩn nằm trong onMutate:
ts
useMutation({
mutationFn: toggleLike,
onMutate: async (postId) => {
await qc.cancelQueries({ queryKey: ['post', postId] }) // 1. huỷ refetch đang chạy
const prev = qc.getQueryData(['post', postId]) // 2. lưu snapshot
qc.setQueryData(['post', postId], (old) => ({ ...old, liked: !old.liked })) // 3. ghi lạc quan
return { prev } // context cho onError
},
onError: (_err, postId, ctx) => {
qc.setQueryData(['post', postId], ctx?.prev) // 4. rollback về snapshot
},
onSettled: (_d, _e, postId) => {
qc.invalidateQueries({ queryKey: ['post', postId] }) // đồng bộ lại với server
},
})Hai chi tiết hay bị hỏi thêm:
- Rollback bằng snapshot chứ không phải "làm ngược lại thao tác". Nếu hai mutation chạy song song, đảo ngược thủ công cho ra kết quả sai; ghi đè bằng snapshot thì trạng thái cuối vẫn hội tụ nhờ onSettled invalidate.
- Với thao tác đơn giản chỉ ảnh hưởng một màn, có thể dùng biến variables của mutation để render trạng thái tạm mà không đụng cache — ít rủi ro hơn nhiều.