TanStack Query: Cách quản lý Server State và Caching chuẩn chỉnh cho React Dev

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Vật lộn với useEffect và mớ code gọi API hỗn độn

Hồi mới học React, mình từng nghĩ useEffect kết hợp useState là đủ để xử lý mọi yêu cầu gọi API. Nhưng thực tế phũ phàng hơn nhiều. Mỗi lần lấy dữ liệu, mình lại phải quản lý ít nhất 3 cái state: data, isLoadingerror. Cứ thế, code trở nên lặp đi lặp lại một cách nhàm chán và cực kỳ khó kiểm soát khi dự án phình to.

Hãy tưởng tượng bạn đang làm một ứng dụng thương mại điện tử. Người dùng nhấn vào xem chi tiết sản phẩm, rồi quay lại danh sách, rồi lại nhấn vào đúng sản phẩm đó. Nếu dùng useEffect thuần, app của bạn sẽ hì hục call API 2 lần cho cùng một nội dung. Điều này không chỉ gây lãng phí tài nguyên server mà còn khiến người dùng phải nhìn cái loading spinner quay vòng vòng một cách vô lý.

Tại sao cách làm cũ lại khiến dự án nhanh chóng “nát”?

Sau nhiều giờ ngồi gỡ lỗi (debug) đến hoa cả mắt, mình nhận ra lỗi lầm lớn nhất là chúng ta đang đánh đồng Client StateServer State:

  • Client State: Là những dữ liệu tạm thời như trạng thái đóng/mở menu, theme sáng/tối. Bạn có toàn quyền kiểm soát nó ngay trong code.
  • Server State: Là dữ liệu nằm trên server (danh sách bài viết, thông tin user). Nó mang tính bất đồng bộ, có thể thay đổi bất cứ lúc nào bởi người khác và quan trọng nhất: app của bạn không thực sự sở hữu nó.

Dùng useEffect nghĩa là bạn đang ép một thứ không ổn định (Server State) hoạt động theo cách thủ công. Chúng ta thiếu một bộ đệm (cache) đủ thông minh để biết khi nào dữ liệu đã cũ (stale) và khi nào cần lấy lại dữ liệu tự động khi người dùng quay lại tab trình duyệt.

Giải pháp nào thực sự đáng đồng tiền bát gạo?

Để giải quyết bài toán này, cộng đồng thường cân nhắc 3 hướng đi chính:

  1. Tự viết Custom Hooks: Cách này giúp giảm lặp code nhưng bạn vẫn phải tự đau đầu xử lý logic caching và đồng bộ dữ liệu giữa 10 component khác nhau.
  2. Dùng Redux Toolkit Query (RTK Query): Một công cụ cực mạnh nếu dự án của bạn đã lỡ “phóng lao” theo Redux. Tuy nhiên, nếu chỉ cần quản lý API thì cấu hình của nó thực sự là một mê cung rườm rà.
  3. TanStack Query (React Query): Đây là lựa chọn thực dụng và tinh gọn nhất. Nó tách biệt hoàn toàn logic lấy dữ liệu ra khỏi giao diện, hỗ trợ caching tự động và cực kỳ dễ triển khai.

Trong quá trình làm việc, khi gặp những API trả về kết quả JSON dài hàng trăm dòng, mình thường dùng toolcraft.app/vi/tools/developer/json-formatter để làm đẹp dữ liệu trước khi định nghĩa interface cho TypeScript. Việc này giúp tiết kiệm ít nhất 15-20 phút so với việc ngồi đọc mớ text dính chùm.

Triển khai TanStack Query: Từ cơ bản đến chuyên nghiệp

1. Thiết lập QueryClient

Bước đầu tiên, bạn cần bọc ứng dụng của mình trong QueryClientProvider. Đây giống như một “trạm trung chuyển” quản lý mọi dữ liệu cache của toàn bộ hệ thống.

npm install @tanstack/react-query
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <MyComponent />
    </QueryClientProvider>
  );
}

2. Lấy dữ liệu mượt mà với useQuery

Thay vì viết 20 dòng code với useEffect, giờ đây bạn chỉ cần vài dòng với hook useQuery. Bí mật nằm ở queryKey – cái tên định danh giúp thư viện nhận diện dữ liệu trong cache.

const { data, isLoading, isError } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  staleTime: 1000 * 60 * 5, // Dữ liệu được coi là "mới" trong vòng 5 phút
});

Kinh nghiệm nhỏ: Hãy tận dụng staleTime. Với những dữ liệu ít biến động như danh mục sản phẩm, bạn nên để thời gian này dài một chút để giảm tải cho server.

3. Optimistic Updates: Bí kíp giúp App chạy “nhanh như điện”

Đây là tính năng đáng giá nhất. Thông thường, khi người dùng nhấn “Like”, họ phải đợi 1-2 giây để API phản hồi rồi nút mới đổi màu. Trải nghiệm này rất rời rạc.

Optimistic Updates cho phép bạn cập nhật giao diện ngay lập tức như thể thao tác đã thành công. Nếu chẳng may server báo lỗi, hệ thống sẽ tự động hoàn tác (rollback) về trạng thái cũ. Người dùng sẽ có cảm giác ứng dụng phản hồi ngay tức thì.

const mutation = useMutation({
  mutationFn: updatePost,
  onMutate: async (newPost) => {
    // Chặn các query khác để tránh xung đột
    await queryClient.cancelQueries({ queryKey: ['posts'] });
    
    // Lưu trạng thái hiện tại để phòng trường hợp lỗi
    const previousPosts = queryClient.getQueryData(['posts']);

    // Cập nhật UI ngay lập tức (Chạy trước cả khi API xong)
    queryClient.setQueryData(['posts'], (old) => 
      old.map(post => post.id === newPost.id ? { ...post, ...newPost } : post)
    );

    return { previousPosts };
  },
  onError: (err, newPost, context) => {
    // Nếu lỗi, trả lại dữ liệu cũ ngay
    queryClient.setQueryData(['posts'], context.previousPosts);
  },
  onSettled: () => {
    // Luôn đồng bộ lại với server sau khi xong
    queryClient.invalidateQueries({ queryKey: ['posts'] });
  },
});

Những bài học xương máu bạn cần nhớ

Sau khi áp dụng TanStack Query vào nhiều dự án thực tế, mình rút ra 3 quy tắc vàng:

  • Cấu trúc QueryKey rõ ràng: Luôn đặt key theo kiểu phân cấp như ['user', userId] hoặc ['products', 'search', query] để dễ dàng quản lý và xóa cache khi cần.
  • Ưu tiên trải nghiệm người dùng: Đừng lúc nào cũng hiện Loading Spinner che kín màn hình. Hãy dùng dữ liệu cũ từ cache và chỉ hiện một thanh loading nhỏ phía trên để tạo cảm giác mượt mà.
  • Luôn cài đặt Devtools: Công cụ này sẽ giúp bạn soi rõ từng query đang hoạt động thế nào, cái nào đang bị cũ để xử lý kịp thời.

Tóm lại, nếu muốn nâng cấp từ một coder lên thành một kỹ sư Frontend chuyên nghiệp, bạn buộc phải nắm vững TanStack Query. Nó không chỉ giúp code sạch hơn mà còn mang lại trải nghiệm người dùng đẳng cấp hơn hẳn so với việc dùng useEffect truyền thống.

Share: