blog.dopana

Back

TanStack Query (React Query) là thư viện quản lý server state cho React. Nó giải quyết: cache, background refetch, stale management, pagination, optimistic update — những thứ Redux/Zustand không làm tốt.

import { useQuery } from '@tanstack/react-query'

function Posts() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['posts'],
    queryFn: () => fetch('/api/posts').then(r => r.json()),
  })

  if (isLoading) return <Spinner />
  if (error) return <Error message={error.message} />
  return <PostList posts={data} />
}
typescript

Không useEffect, không useState, không manual loading/error state.

Vấn Đề Server State#

Server state khác client state:

Client StateServer State
Theme, modal, form inputDữ liệu từ API
Chỉ UI quan tâmNhiều component dùng chung
Sync ngayCó thể stale
Không ai khác sửaNgười dùng khác có thể sửa

Với useEffect + useState thủ công:

TanStack Query xử lý tất cả.

Core Concepts#

useQuery#

const { data, isLoading, isError, error, isFetching, refetch } = useQuery({
  queryKey: ['posts'],           // Key duy nhất cho cache
  queryFn: () => fetchPosts(),   // Hàm fetch dữ liệu
  staleTime: 5 * 60 * 1000,     // 5 phút mới stale
  gcTime: 30 * 60 * 1000,       // Giữ cache 30 phút (trước là cacheTime)
  refetchOnWindowFocus: true,    // Tự động refetch khi focus tab
  retry: 3,                      // Retry 3 lần khi lỗi
  enabled: true,                 // Conditional fetching
})
typescript

Query Key#

// Key đơn giản
useQuery({ queryKey: ['posts'], queryFn: ... })

// Key với params — tự động refetch khi key thay đổi
useQuery({
  queryKey: ['posts', { page, limit, search }],
  queryFn: () => fetchPosts({ page, limit, search }),
})

// Key best practice: ['domain', 'action', ...params]
['posts']                          // Tất cả posts
['posts', postId]                  // Một post
['posts', postId, 'comments']      // Comments của một post
['users', userId, 'profile']       // Profile của user
typescript

useMutation#

Patterns#

Dependent Queries#

// Query này phụ thuộc kết quả query kia
const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
})

const { data: posts } = useQuery({
  queryKey: ['posts', user?.id],
  queryFn: () => fetchPostsByUser(user!.id),
  enabled: !!user,  // Chỉ chạy khi có user
})
typescript

Parallel Queries#

// Tự động chạy song song
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
const postsQuery = useQuery({ queryKey: ['posts'], queryFn: fetchPosts })
const commentsQuery = useQuery({ queryKey: ['comments'], queryFn: fetchComments })

// Dùng useQueries cho array động
const postIds = [1, 2, 3, 4, 5]
const postsQueries = useQueries({
  queries: postIds.map(id => ({
    queryKey: ['posts', id],
    queryFn: () => fetchPost(id),
  })),
})
typescript

Pagination#

Infinite Scroll#

Optimistic Update#

Cache Configuration#

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,      // 5 phút — dữ liệu được coi là fresh
      gcTime: 30 * 60 * 1000,        // Giữ cache 30 phút sau khi unmount
      refetchOnWindowFocus: true,     // Refetch khi focus tab
      refetchOnReconnect: true,       // Refetch khi online lại
      retry: 3,
      retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
    },
  },
})
typescript

Per-query override#

// Dữ liệu ổn định — ít refetch
useQuery({
  queryKey: ['settings'],
  queryFn: fetchSettings,
  staleTime: 30 * 60 * 1000,     // 30 phút
  refetchOnWindowFocus: false,
})

// Dữ liệu real-time — luôn fresh
useQuery({
  queryKey: ['notifications'],
  queryFn: fetchNotifications,
  staleTime: 0,                   // Luôn stale → refetch khi focus
  refetchInterval: 10000,         // Poll mỗi 10s
})
typescript

DevTools#

npm install @tanstack/react-query-devtools
bash
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}
typescript

DevTools cho phép: inspect cache, manual refetch, reset query, xem trạng thái (fresh/stale/fetching).

Custom Hooks Pattern#

Khi Nào Dùng TanStack Query?#

✅ Nên dùng#

  • Bất kỳ React app nào gọi API
  • Dashboard với nhiều data fetching
  • Real-time UI cần sync với server
  • App có pagination/infinite scroll
  • Optimistic update cần rollback

❌ Không cần#

  • App không gọi API (static site)
  • Dùng tRPC (tRPC tích hợp React Query sẵn)
  • Server-rendered app không có client fetch

Tổng Kết#

TanStack Query loại bỏ ~90% boilerplate data fetching trong React.