TanStack Query · Phần 18 — Kiến trúc production & Migration (Capstone)
Gói 18 phần thành một query layer theo feature: cấu trúc thư mục, quy ước loading/error, devtools & logging cho production, lộ trình migrate v4 → v5, so sánh với RTK Query/SWR, và checklist production-ready.
Đây là bài kết của series. Mười bảy phần trước cho bạn từng mảnh — query, cache, mutation, SSR, offline, realtime, hiệu năng, type. Phần này lắp chúng thành một kiến trúc mà cả team áp dụng được, rồi cho bạn lộ trình migrate, so sánh với hàng xóm, và một checklist để tự chấm điểm dự án.
Mục tiêu: rời series với một bản thiết kế trong đầu cho lớp data của bất kỳ app React nào.
1. Cấu trúc thư mục theo feature
Đừng gom mọi query vào một file queries.ts khổng lồ. Tổ chức theo feature, mỗi feature tự chứa key, schema, api, query/mutation options và hook:
src/
├── lib/
│ ├── query-client.ts # QueryClient + defaultOptions + caches (Phần 9)
│ └── api-client.ts # apiFetch + ApiError + zod helper (Phần 3)
├── app/
│ └── providers.tsx # QueryClientProvider / Persist / Hydration
└── features/
└── customers/
├── keys.ts # customerKeys (factory phân cấp)
├── schema.ts # zod schema + z.infer types
├── api.ts # fetchCustomers, createCustomer... (throw ApiError)
├── queries.ts # queryOptions: customersQuery, customerQuery
├── mutations.ts # mutationOptions: createCustomerMutation...
└── hooks.ts # useCustomers, useCreateCustomer (gói lại cho UI)
Quy tắc: component chỉ import từ hooks.ts, không bao giờ gọi useQuery với key/fn tay trong component. Điều này giữ key tập trung, type nhất quán, và đổi data layer không phải sửa component.
// features/customers/hooks.ts — biên giới UI dùng
import { useQuery } from '@tanstack/react-query';
import { customersQuery } from './queries';
export function useCustomers() {
return useQuery(customersQuery());
}
2. Quy ước loading/error nhất quán
Mỗi team nên chốt một cách xử lý loading/error và áp khắp nơi. Hai lựa chọn chính (Phần 7):
| Cách | Loading | Error | Hợp khi |
|---|---|---|---|
| Imperative | if (isPending) | if (isError) | Cần kiểm soát inline, granular |
| Declarative | <Suspense> | <ErrorBoundary> | Muốn component sạch, loading/error gom ở biên |
Chọn declarative cho app mới: useSuspenseQuery + Suspense/ErrorBoundary cho từng route, cộng một QueryCache.onError toàn cục (Phần 9) cho toast/log. Đừng trộn hai cách trong cùng một feature — gây khó đoán.
3. Devtools & logging trong production
Devtools mặc định chỉ vào dev. Nhưng đôi khi cần debug production (bug chỉ xảy ra trên prod). Lazy-load Devtools có điều kiện:
import { lazy, Suspense } from 'react';
const ReactQueryDevtoolsProd = lazy(() =>
import('@tanstack/react-query-devtools/production').then((d) => ({
default: d.ReactQueryDevtools,
})),
);
function App() {
const showDevtools = useDevtoolsToggle(); // vd bật qua localStorage flag
return (
<>
{/* ...app... */}
{showDevtools && (
<Suspense fallback={null}>
<ReactQueryDevtoolsProd />
</Suspense>
)}
</>
);
}
Cho logging/observability, gắn vào cache (Phần 12):
queryClient.getQueryCache().subscribe((event) => {
if (event.type === 'updated' && event.query.state.status === 'error') {
logToObservability('query_error', {
key: event.query.queryKey,
error: event.query.state.error,
});
}
});
4. Migrate v4 → v5 — những đổi thay phải biết
Nếu kế thừa codebase v4, đây là các breaking change quan trọng nhất:
| v4 | v5 | Ghi chú |
|---|---|---|
isLoading | isPending | isLoading giờ = isPending && isFetching |
cacheTime | gcTime | Đổi tên, ý nghĩa giữ nguyên |
useQuery(key, fn, opts) | useQuery({ queryKey, queryFn, ...opts }) | Chỉ còn một object argument |
onSuccess/onError/onSettled trên useQuery | Đã bỏ | Chuyển sang QueryCache global hoặc useEffect |
keepPreviousData: true | placeholderData: keepPreviousData | Import keepPreviousData |
useQueries trả mảng | thêm combine | (Phần 16) |
Bỏ callback trên useQuery là thay đổi gây “đau” nhất. Lý do: chúng chạy không đoán được với cache chia sẻ. Thay thế: side-effect “sau khi data về” chuyển sang useEffect theo data, còn xử lý global (toast/log) lên QueryCache.onError.
Có codemod chính thức:
npx jscodeshift ... @tanstack/react-query/codemods/v5/...cho phần đổi tên và gộp argument. Chạy nó trước, rồi dọn tay phần callback.
5. So sánh nhanh: RTK Query & SWR
| TanStack Query | RTK Query | SWR | |
|---|---|---|---|
| Gắn với | Độc lập (mọi nguồn async) | Redux Toolkit | Next.js-friendly, nhẹ |
| Cache | Mạnh, linh hoạt, thao tác chủ động | Tốt, theo endpoint | Đơn giản, key-based |
| Mutation/optimistic | Đầy đủ, kiểm soát cao | Có | Cơ bản |
| Offline/persist | Chính thức (Phần 13) | Hạn chế | Qua middleware |
| Khi nào chọn | App có server state phức tạp, cần kiểm soát | Đã dùng Redux nặng, thích codegen từ OpenAPI | App nhỏ/vừa, ưu tiên đơn giản |
TanStack Query thắng khi server state là trung tâm và bạn cần toàn quyền với cache. SWR thắng khi muốn tối giản. RTK Query hợp khi team đã sống trong Redux.
6. Checklist production-ready
Chấm dự án của bạn theo danh sách này:
Cấu hình
- Một
QueryClientmodule-scope (browser) / mới mỗi request (server) -
defaultOptionscóstaleTime > 0,retrybỏ qua 4xx,gcTimehợp lý -
QueryCache.onError/MutationCache.onErrortoàn cục (401 → logout, toast)
Tổ chức
- Query/mutation gói trong
queryOptions/mutationOptions, không key/fn tay trong component - Key factory phân cấp tập trung mỗi feature
- Component chỉ import qua hook layer
Type & data
- Validate biên bằng zod;
type = z.infer<schema>; khôngany, khôngasngoài sau parse -
skipTokencho dependent query
Hiệu năng
- Subscription đặt gần nơi dùng data
- Prefetch theo ý định / route loader cho điều hướng chính
- Tránh waterfall (
Promise.all,useSuspenseQueries)
Bền & UX
- Quy ước loading/error nhất quán (Suspense+Boundary hoặc imperative)
- Optimistic update có cancel → snapshot → patch → rollback → invalidate
- (Nếu offline) persist + paused mutations +
bustertheo schema
Chất lượng
- Test hook với MSW (Phần 8)
- Devtools sẵn sàng cho dev (và bật được ở prod khi cần)
7. Bài tập
1. Vì sao nên buộc component import qua “hook layer” thay vì gọi useQuery trực tiếp với key/fn?
Lời giải
Hook layer tập trung key, type và cấu hình ở một chỗ: đổi cách fetch, đổi key, thêm select chỉ sửa một file mà không đụng component. Gọi useQuery tay rải rác dễ gây lệch key (hỏng cache/invalidation), lặp type, và khó refactor. Đó là ranh giới sạch giữa data layer và UI.
2. Thay đổi v4 → v5 nào gây tái cấu trúc nhiều nhất, và thay thế ra sao?
Lời giải
Bỏ onSuccess/onError/onSettled trên useQuery (chúng chạy không đoán được với cache chia sẻ). Thay thế: side-effect theo data chuyển sang useEffect phụ thuộc data; xử lý toàn cục (toast/log/logout) lên QueryCache.onError. (Các đổi tên isLoading→isPending, cacheTime→gcTime, gộp argument có codemod lo.)
3. Khi nào TanStack Query là lựa chọn đúng hơn SWR hay RTK Query?
Lời giải
Khi server state phức tạp và bạn cần toàn quyền: thao tác cache chủ động, optimistic đa query, offline/persist chính thức, realtime patch, kiểm soát re-render chi tiết. SWR hợp app nhỏ/vừa ưu tiên đơn giản; RTK Query hợp team đã dùng Redux nặng và thích codegen từ OpenAPI.
Nâng cao: Lấy một feature trong dự án, tái cấu trúc theo layout mục 1 (keys/schema/api/queries/mutations/hooks), rồi chạy checklist mục 6 và sửa mọi mục chưa đạt. Đây là “capstone” thật của series.
Tóm tắt
- Tổ chức theo feature: keys/schema/api/queries/mutations/hooks; component chỉ chạm hook layer.
- Chốt một quy ước loading/error; ưu tiên declarative +
QueryCache.onErrortoàn cục. - Devtools lazy-load được ở prod; gắn observability vào cache subscribe.
- Migrate v4→v5: chạy codemod cho đổi tên/gộp argument, dọn tay phần callback
useQueryđã bỏ. - Chọn TanStack Query khi server state phức tạp; SWR cho tối giản; RTK Query cho hệ Redux.
- Dùng checklist production-ready để tự chấm điểm dự án.
Toàn series
- Phần 1 — Mental Model & Cài đặt
- Phần 2 — useQuery sâu & vòng đời cache
- Phần 3 — Query keys, zod & queryOptions
- Phần 4 — Pagination & useInfiniteQuery
- Phần 5 — Mutations & Invalidation
- Phần 6 — Optimistic Updates & quản lý cache
- Phần 7 — Errors, Retry, Suspense & Performance
- Phần 8 — Testing & Capstone
- Phần 9 — QueryClient & Defaults sâu
- Phần 10 — SSR, Next.js App Router & Hydration
- Phần 11 — Prefetching & tích hợp Router nâng cao
- Phần 12 — QueryClient như một store
- Phần 13 — Offline-first & Persistence sâu
- Phần 14 — Realtime: WebSocket, SSE & cache
- Phần 15 — Mutation nâng cao
- Phần 16 — Hiệu năng render sâu
- Phần 17 — Type-safety đỉnh cao
- Phần 18 — Kiến trúc production & Migration (bạn đang ở đây)
Điều cốt lõi
TanStack Query không chỉ là “fetch giúp bạn” — nó là một lớp đồng bộ server state có cache thông minh, mutation có kiểm soát, hỗ trợ SSR/offline/realtime, và type chặt. Dùng đúng, nó xoá hàng nghìn dòng boilerplate và biến những bài toán khó (race, optimistic, offline) thành pattern có sẵn. Giờ bạn đã có cả 18 mảnh — hãy ghép chúng vào dự án thật.