Optimistic UI = cập nhật giao diện ngay, gửi request ở nền, hoàn tác nếu thất bại. Phần khó không phải lần bấm đầu mà là nhiều lần bấm chồng nhau.
Luồng chuẩn của một mutation optimistic:
1. Huỷ các query đang bay cho cùng dữ liệu — nếu không, response cũ về sau sẽ ghi đè giá trị vừa cập nhật lạc quan.
2. Lưu snapshot giá trị hiện tại.
3. Ghi giá trị mới vào cache.
4. Lỗi thì rollback bằng snapshot; xong thì invalidate để lấy con số thật từ server.
js
useMutation({
mutationFn: toggleLike,
onMutate: async (postId) => {
await queryClient.cancelQueries({ queryKey: ['post', postId] })
const prev = queryClient.getQueryData(['post', postId])
queryClient.setQueryData(['post', postId], (p) => ({
...p, liked: !p.liked, likes: p.likes + (p.liked ? -1 : 1),
}))
return { prev }
},
onError: (_e, postId, ctx) => queryClient.setQueryData(['post', postId], ctx.prev),
onSettled: (_d, _e, postId) => queryClient.invalidateQueries({ queryKey: ['post', postId] }),
})Bấm liên tục sinh ba vấn đề:
- Nhiều request cùng lúc: hoặc đặt mutation dạng chỉ một cái chạy tại một thời điểm, hoặc gửi trạng thái mong muốn (
liked: true) thay vì lệnhtoggle, để việc gửi lại nhiều lần cho cùng kết quả (idempotent). - Rollback sai: nếu có N mutation chồng nhau, rollback về snapshot của mutation đầu sẽ xoá luôn thay đổi hợp lệ ở giữa. Cách an toàn là chỉ rollback khi không còn mutation nào đang chạy, rồi
invalidatecho server chốt lại. - Với giỏ hàng, con số phụ thuộc tồn kho nên server có quyền từ chối. Rollback phải kèm thông báo rõ ("Sản phẩm đã hết hàng"), không im lặng đưa số về cũ.
Chỉ dùng optimistic khi khả năng thất bại thấp và hậu quả nhẹ (like, bookmark, đổi số lượng). Thanh toán hay xoá dữ liệu thì hiển thị trạng thái chờ thật, đừng báo thành công trước.