Skip to content
Home
Go back

Optimistic Updates với React Query

Mục lục

Xem mục lục

Optimistic Update là gì

Optimistic Update là cập nhật UI ngay lập tức, dựa trên giả định thao tác sẽ thành công, thay vì đợi server xác nhận rồi mới hiển thị kết quả. Nếu request thất bại, UI được rollback về trạng thái trước đó.

Cách này đánh đổi độ chính xác tức thời lấy cảm giác phản hồi nhanh: người dùng thấy kết quả ngay khi bấm, không phải chờ round-trip mạng, dù bản chất dữ liệu thật vẫn chưa được server xác nhận tại thời điểm đó.

Cách làm thông thường (không optimistic)

Mặc định, một mutation chờ response rồi mới cập nhật UI:

function useToggleLike(postId) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: () => api.toggleLike(postId),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["posts", postId] });
    },
  });
}

Người dùng bấm nút “Like”, nút vẫn giữ trạng thái cũ cho tới khi request xong (thường vài trăm mili-giây tới vài giây tùy mạng), rồi mới cập nhật lại UI theo dữ liệu mới từ invalidateQueries. Với mạng chậm, độ trễ này cảm nhận được rõ rệt, tạo cảm giác ứng dụng phản hồi chậm.

useMutation và onMutate

onMutate chạy ngay trước khi request được gửi đi, là nơi thực hiện optimistic update: ghi thẳng dữ liệu mới vào cache của React Query trước khi biết kết quả thật.

function useToggleLike(postId) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: () => api.toggleLike(postId),
    onMutate: async () => {
      await queryClient.cancelQueries({ queryKey: ["posts", postId] });

      queryClient.setQueryData(["posts", postId], old => ({
        ...old,
        liked: !old.liked,
        likeCount: old.liked ? old.likeCount - 1 : old.likeCount + 1,
      }));
    },
  });
}

cancelQueries hủy các query đang chạy tới cùng queryKey, tránh tình huống một response cũ trả về sau và ghi đè lên optimistic update vừa thực hiện. setQueryData cập nhật thẳng vào cache, khiến UI re-render ngay lập tức với dữ liệu mới, dù request thật vẫn đang chạy ngầm.

Lưu lại snapshot để rollback

Nếu request thất bại, cần khôi phục cache về đúng trạng thái trước khi optimistic update. Muốn vậy phải lưu lại giá trị cũ trước khi ghi đè, thường trả về từ onMutate dưới dạng context:

onMutate: async () => {
  await queryClient.cancelQueries({ queryKey: ["posts", postId] });

  const previousPost = queryClient.getQueryData(["posts", postId]);

  queryClient.setQueryData(["posts", postId], old => ({
    ...old,
    liked: !old.liked,
    likeCount: old.liked ? old.likeCount - 1 : old.likeCount + 1,
  }));

  return { previousPost };
},

context (giá trị onMutate trả về) được React Query tự động truyền vào onError và onSettled, không cần lưu ở biến ngoài hay state riêng.

onError: rollback khi request thất bại

onError: (err, variables, context) => {
  if (context?.previousPost) {
    queryClient.setQueryData(["posts", postId], context.previousPost);
  }
},

Khi mutationFn ném lỗi, React Query gọi onError với đúng context đã lưu ở onMutate, dùng nó để ghi đè cache về giá trị cũ, đưa UI trở lại trạng thái trước khi optimistic update, đúng như chưa từng có gì xảy ra.

onSettled: đồng bộ lại với server

onSettled: () => {
  queryClient.invalidateQueries({ queryKey: ["posts", postId] });
},

onSettled luôn chạy sau cùng, bất kể mutation thành công hay thất bại. Gọi invalidateQueries ở đây đảm bảo cache cuối cùng luôn khớp với dữ liệu thật từ server, kể cả khi optimistic update đã đoán đúng (server có thể trả về dữ liệu hơi khác, ví dụ likeCount bị thay đổi bởi user khác cùng lúc).

Ví dụ đầy đủ: optimistic update cho danh sách todo

function useToggleTodo() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: todoId => api.toggleTodo(todoId),

    onMutate: async todoId => {
      await queryClient.cancelQueries({ queryKey: ["todos"] });

      const previousTodos = queryClient.getQueryData(["todos"]);

      queryClient.setQueryData(["todos"], old =>
        old.map(todo =>
          todo.id === todoId ? { ...todo, done: !todo.done } : todo
        )
      );

      return { previousTodos };
    },

    onError: (err, todoId, context) => {
      if (context?.previousTodos) {
        queryClient.setQueryData(["todos"], context.previousTodos);
      }
    },

    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["todos"] });
    },
  });
}

Sử dụng:

function TodoItem({ todo }) {
  const toggleTodo = useToggleTodo();

  return (
    <label>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={() => toggleTodo.mutate(todo.id)}
      />
      {todo.title}
    </label>
  );
}

Checkbox đổi trạng thái ngay khi click, không đợi request. Nếu request thất bại (mất mạng, server lỗi), checkbox tự động trở lại trạng thái cũ nhờ onError, người dùng thấy rõ thao tác không thành công mà không cần thông báo lỗi riêng.

Khi nào nên và không nên dùng Optimistic Update

Nên dùng với thao tác:

Không nên dùng với thao tác:

Nguyên tắc chung: optimistic update phù hợp nhất với thao tác nhỏ, tần suất cao, ít rủi ro. Thao tác quan trọng, ít khi lặp lại, nên ưu tiên độ chính xác hơn tốc độ cảm nhận.

Tổng kết


Share this post:

Previous Post
Component Lifecycle trong React: Mounting, Updating, Unmounting
Next Post
Redux: Store, Action, Reducer và Redux Toolkit