· tanstack-query / swr / react
TanStack Query vs SWR — server state trong năm 2026
TanStack Query dẫn đầu về tính năng, devtools và momentum. SWR dẫn đầu về bundle size và tính đơn giản. Đây là lúc mỗi lựa chọn phù hợp với app của bạn.
Bởi Ethan
2.359 từ · 12 phút đọc
Dùng TanStack Query nếu app của bạn có cache invalidation phức tạp, background mutation, hoặc đội nhóm sẽ sống trong devtools. Dùng SWR nếu bạn cần một thư viện native của Vercel làm tốt một việc và không bao giờ cản trở bạn. Cả hai đều được bảo trì tích cực. Không cái nào sai. Câu hỏi là bạn đang tối ưu cho điều gì.
Bài này dành cho ai
React developer đang cân nhắc giữa hai lựa chọn cho dự án mới, hoặc đang tính đến việc migrate. Nếu bạn đã dùng một trong hai trên production và không gặp vấn đề gì, hãy giữ nguyên — đổi thư viện chỉ vì muốn đổi sẽ tốn công hơn những gì bạn tiết kiệm được.
Những gì chúng tôi kiểm tra
TanStack Query v5.100.10 và SWR v2.4.1, cả hai tính đến tháng 5 năm 2026. Dữ liệu lấy từ changelogs chính thức, npm registry, bundlephobia, và các GitHub repo. Con số download từ npm stats. Đây là bài so sánh, không phải benchmark — khi methodology quan trọng, chúng tôi sẽ nói rõ.
Lý do bài so sánh này vẫn còn giá trị trong năm 2026
Hầu hết các bài “TanStack Query vs SWR” đang lưu hành so sánh react-query v3 với SWR v1. Đó là một cuộc đua hoàn toàn khác. Cả hai thư viện đã ra phiên bản lớn kể từ đó, và khoảng cách giữa chúng đã thay đổi.
TanStack Query v5 ra mắt vào tháng 10 năm 2023 và được bảo trì tích cực từ đó — repo có commit ngay trong ngày 2026-05-17. SWR v2 ra mắt tháng 12 năm 2022 và đạt đến v2.4.1 vào tháng 2 năm 2026. API của cả hai thay đổi đủ nhiều so với phiên bản trước đến mức các lời khuyên cũ thường dẫn bạn đến những pattern không còn tồn tại.
React Server Components và Server Actions cũng đã thay đổi cục diện. Cách mỗi thư viện hoạt động trong một App Router app không hề rõ ràng từ tài liệu thời v1.
Về mặt momentum: download của TanStack Query đạt 12.3 triệu/tuần vào tháng 5 năm 2026, gấp khoảng 2.5 lần so với 4.85 triệu/tuần của SWR. Khoảng cách này ngày càng mở rộng qua từng năm kể từ 2024. User base của SWR vẫn ổn định nhưng hẹp hơn, tập trung trong hệ sinh thái Vercel.
Nhận định nhanh
| Tiêu chí | TanStack Query v5.100.10 | SWR v2.4.1 |
|---|---|---|
| Bundle (minzipped) | ~13.4 KB | ~4.2 KB |
| npm downloads/tuần | 12.3M | 4.85M |
| GitHub stars | 49,444 | 32,382 |
| TypeScript | Typed errors, generics xuyên suốt | Fully typed, inference đơn giản hơn |
| DevTools | Panel nổi tích hợp sẵn | Extension trình duyệt từ cộng đồng |
| Tích hợp Next.js / RSC | Dehydration + pending-query streaming | Sản phẩm native của Vercel, pattern prefetch đơn giản hơn |
| Kiểm soát cache | staleTime, gcTime, networkMode, maxPages | revalidateOnFocus, dedupingInterval |
| Mutation DX | useMutation — verbose nhưng chính xác | useSWRMutation — gọn hơn cho write đơn giản |
| Độ khó học | Cao hơn (~20 config options) | Thấp: một hook, defaults hợp lý |
Phân tích chi tiết: TanStack Query v5
API chuyển sang single-object signature
v5 bỏ positional overload. Mỗi hook giờ chỉ nhận một options object:
// v4
useQuery(['user', id], fetchUser, { staleTime: 60_000 })
// v5
useQuery({ queryKey: ['user', id], queryFn: fetchUser, staleTime: 60_000 })
v5 đi kèm một codemod để xử lý việc migration tự động. Đây là breaking change, nhưng nó giúp query config trở nên portable — bạn có thể trích xuất vào helper queryOptions() và dùng lại ở nhiều component và server-side call mà không cần lặp lại options.
cacheTime đổi tên thành gcTime
Tên mới làm rõ hơn chức năng thực sự của setting. gcTime là thời điểm các query không còn dùng đến sẽ bị thu dọn khỏi bộ nhớ. staleTime là thời gian dữ liệu được coi là còn “tươi” trước khi một background refetch xảy ra. Nhầm lẫn giữa hai khái niệm này là nguyên nhân phổ biến nhất gây ra hành vi khó hiểu trong v4.
Typed errors
v5 mặc định kiểu của error là Error thay vì unknown. Kết hợp với option throwOnError mới (thay thế cho useErrorBoundary đã deprecated), bạn có type safety đầy đủ trong error path mà không cần cast.
Suspense hooks
useSuspenseQuery, useSuspenseInfiniteQuery, và useSuspenseQueries đã stable trong v5. Chúng đảm bảo data không thể là null ở cấp độ type, nghĩa là không cần if (!data) return null trong Suspense tree. Component hoặc là throw một promise (suspend), hoặc render với data. Không có trạng thái thứ ba nào cần xử lý.
Pending-query streaming (v5.40+)
Đây là tính năng v5 có nhiều khả năng tạo ra sự khác biệt nhất cho các App Router app. Thư viện giờ có thể serialize các query đang chạy và dehydrate chúng cho client. Một Next.js page có thể bắt đầu stream HTML ngay lập tức trong khi query vẫn đang chạy trên server. Client hydrate vào cùng trạng thái pending — không có waterfall, không có flash loading, không có double fetch.
DevTools
@tanstack/react-query-devtools là một panel nổi được lazy-load trong môi trường development (tree-shaken trong production). Nó hiển thị mọi query key, trạng thái, đếm ngược staleTime, cache data, và các network event. Nếu bạn đang debug cache invalidation qua một component tree lớn, đây là công cụ khiến việc đó trở nên khả thi.
Hỗ trợ framework
v5 đi kèm adapter cho React, Vue, Svelte, Angular, Solid, và (tính đến năm 2026) Lit. Core hoàn toàn framework-agnostic. Với các đội nhóm không chỉ làm React, điều này có ý nghĩa quan trọng.
Phân tích chi tiết: SWR v2
useSWRMutation — khoảng trống mà v1 để lại
SWR v1 không có primitive mutation chính thức. Các đội nhóm phải xử lý bằng cách gọi global mutate sau một fetch thông thường — cách này hoạt động được nhưng để lại việc kết nối loading và error state cho bạn tự lo. v2 giải quyết vấn đề này với useSWRMutation:
const { trigger, isMutating } = useSWRMutation('/api/users', createUser, {
optimisticData: (currentData) => [...currentData, optimisticUser],
populateCache: true,
rollbackOnError: true,
})
trigger được gọi tường minh, isMutating là state chính thức, vòng đời optimistic tích hợp sẵn. Với các app thiên về write, đây là lý do v2 trở thành lựa chọn nghiêm túc ở chỗ mà v1 chỉ là giải pháp tạm.
Global mutate theo điều kiện
v2 cho phép bạn invalidate cache theo điều kiện:
// Invalidate toàn bộ key bắt đầu bằng '/api/user'
mutate((key) => typeof key === 'string' && key.startsWith('/api/user'))
Trước đây bạn phải truyền key chính xác. Với các app có dynamic route, đây là sự khác biệt giữa invalidate đúng chỗ và xóa sạch toàn bộ cache.
isLoading vs isValidating
v1 chỉ có isValidating, luôn là true trong bất kỳ lần fetch nào — lần đầu, background revalidation, hay refresh khi focus. v2 bổ sung isLoading, chỉ là true khi chưa có data và lần fetch đầu tiên đang chạy. Điều này khớp với mental model mà hầu hết UI cần: chỉ hiển thị skeleton ở lần tải đầu, không phải mỗi lần refresh nền.
Lợi thế Vercel
SWR là sản phẩm của Vercel. Đội ngũ Next.js tự dùng nó, và pattern SSR với App Router rất đơn giản:
// app/users/page.tsx — Server Component
export default async function UsersPage() {
const users = await fetchUsers()
return (
<SWRConfig value={{ fallback: { '/api/users': users } }}>
<UserList />
</SWRConfig>
)
}
// UserList.tsx — Client Component
'use client'
function UserList() {
const { data } = useSWR('/api/users', fetcher)
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
Client component render ngay lập tức từ fallback, rồi revalidate trong nền. Không cần khởi tạo QueryClient. Không có provider nào cần setup.
Giới hạn của pattern này: SWR hook chỉ hoạt động phía client. Để streaming pending-query qua ranh giới RSC/RCC, TanStack Query có hạ tầng phù hợp hơn.
Ưu thế về sự đơn giản
Mental model cốt lõi của SWR: một key, một fetcher, một hook. Không cần QueryClient. Không cần Provider theo mặc định. Với một đội chủ yếu cần cache GET request và revalidate khi focus, SWR mất một buổi chiều để học và rất lâu mới cảm thấy thiếu. Đó là tính năng thực sự, không phải hạn chế.
Đối đầu trực tiếp: cùng một bài toán
Bài toán: Fetch một user profile, hiển thị trạng thái loading, xử lý lỗi, và refetch khi cửa sổ được focus lại.
TanStack Query v5
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 60_000, gcTime: 300_000 } },
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<UserProfile userId="123" />
</QueryClientProvider>
)
}
function UserProfile({ userId }: { userId: string }) {
const { data, isPending, isError, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
})
if (isPending) return <Spinner />
if (isError) return <ErrorMessage error={error} />
return <div>{data.name}</div>
}
SWR v2
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then(r => r.json())
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useSWR(
`/api/users/${userId}`,
fetcher,
{ revalidateOnFocus: true }
)
if (isLoading) return <Spinner />
if (error) return <ErrorMessage error={error} />
return <div>{data.name}</div>
}
SWR ít code hơn khoảng 40% cho use case này. Không cần setup provider, không QueryClient, không defaultOptions nào cần cấu hình. Sự verbose của TanStack Query xứng đáng khi bạn cần kiểm soát cache chi tiết, typed errors, devtools, hoặc chia sẻ cache giữa các component với ngữ nghĩa vòng đời rõ ràng.
Nếu bạn đang khám phá cả hai API từ đầu, inline autocomplete của Cursor đặc biệt hiệu quả trong việc điền queryKey generics và gợi ý hook option phù hợp — cả hai thư viện đều có bề mặt option dày đặc và typed inference mà autocomplete xử lý tốt.
Khi nào nên chọn cái nào
Chọn TanStack Query v5 khi
- Cache invalidation phức tạp — bạn cần invalidate nhiều query key sau một mutation, hoặc điều phối giữa các cache liên quan.
- Background mutation ở quy mô lớn —
useMutationvới vòng đờionMutate/onError/onSettledđã được kiểm chứng trong production và dễ kết hợp. - TypeScript nghiêm ngặt quan trọng với đội nhóm — typed errors, typed return type của
queryFn, helperqueryOptions()cho config portable. - Bạn cần devtools — panel tích hợp sẵn tốt hơn hẳn bất kỳ thứ gì SWR cung cấp.
- Next.js App Router + streaming — dehydrate pending query và stream chúng về client đòi hỏi hạ tầng của TanStack Query.
- Đội nhóm đa framework — cùng một thư viện trên React, Vue, Angular, Svelte.
- tRPC — adapter React chính thức của tRPC được xây dựng trên TanStack Query. Nếu bạn dùng tRPC, bạn đã dùng nó rồi.
Chọn SWR v2 khi
- Vercel / Next.js Pages Router — SWR là lựa chọn được khuyến nghị chính thức và pattern trong tài liệu là con đường đơn giản nhất.
- Bundle size quan trọng — 4.2 KB so với 13.4 KB minzipped là sự khác biệt thực sự với các app nhạy cảm về bundle.
- CRUD đơn giản — nếu data model của bạn là “fetch URL này và hiển thị lên,” model một hook của SWR không gì sánh bằng ở quy mô đó.
- Đội nhóm mới với React state management — overhead nhận thức thấp hơn, ít bẫy hơn, nhanh đến giai đoạn productive hơn.
- Prototype nhanh — không cần setup dài dòng, defaults hợp lý, dùng được ngay mà không cần cấu hình gì.
Nhận định
TanStack Query là thư viện lớn hơn, nhiều tính năng hơn. SWR tinh gọn và ít ồn ào hơn. Chúng không thực sự cạnh tranh cho cùng một đối tượng người dùng.
Nếu bạn đang trên stack Next.js/Vercel với nhu cầu data đơn giản: SWR. Nếu bạn đang xây dựng dashboard phức tạp, sản phẩm B2B với nhiều mutation và cache coordination, hoặc app đa framework: TanStack Query. Nếu bạn bắt đầu từ đầu và chưa chắc: chọn TanStack Query và học dần. Migrate ra khỏi SWR sau này tốn công hơn việc học TanStack Query ngay từ đầu.
Cả hai thư viện chỉ xử lý server state. Nếu bạn cần thêm client state, xem bài so sánh Zustand vs Redux Toolkit của chúng tôi.
Lưu ý
- Các con số trong bảng “Nhận định nhanh” được ghi nhận vào tháng 5 năm 2026. Download count và star count thay đổi theo thời gian — hãy kiểm tra lại trên npm trước khi trích dẫn.
- Bài này không có benchmark hiệu năng runtime. Cả hai thư viện đều dùng
useSyncExternalStorenội bộ (concurrent-safe với React 18) và hoạt động tương đương ở cấp độ React scheduler. Không có benchmark độc lập giai đoạn 2025–2026 với methodology đáng tin cậy nào tồn tại tính đến thời điểm viết bài. - Chúng tôi không kiểm tra trên native mobile. Hành vi trên React Native có thể khác, đặc biệt với offline/background sync.
- Bài viết này chứa affiliate link cho Cursor. Chúng tôi dùng Cursor. Điều đó không ảnh hưởng đến nhận định về bất kỳ thư viện nào.
Tham khảo
- TanStack Query overview — 2026-05-17
- TanStack Query v5 migration guide — 2026-05-17
- TanStack Query Advanced SSR — 2026-05-17
- TanStack Query GitHub — 2026-05-17
- SWR homepage — 2026-05-17
- SWR v2 release post — 2026-05-17
- SWR with Next.js — 2026-05-17
- SWR GitHub — 2026-05-17
- @tanstack/react-query on bundlephobia — 2026-05-17
- swr on bundlephobia — 2026-05-17
- npm trends comparison — 2026-05-17