TanStack Query · Phần 11 — Prefetching & tích hợp Router nâng cao
Tải data trước theo ý định người dùng: prefetch on hover/focus/viewport, ensureQueryData vs prefetchQuery, prefetchInfiniteQuery, route loader (React Router & TanStack Router) và cách triệt tiêu request waterfall.
Phần 7 đã giới thiệu prefetch-on-hover và ensureQueryData trong route loader. Phần này đào sâu toàn bộ nghệ thuật prefetch — vũ khí mạnh nhất để app cảm giác tức thì mà không cần server nhanh hơn. Ý tưởng cốt lõi: con người để lộ ý định trước khi hành động (rê chuột, focus, cuộn tới gần), và ta dùng những tín hiệu đó để nạp data sớm.
Ta cũng mổ xẻ kẻ thù số một của tốc độ cảm nhận: request waterfall — chuỗi request nối đuôi tuần tự khi đáng lẽ chúng chạy song song. Và ta sẽ thấy router (React Router v7, TanStack Router) biến prefetch thành một phần kiến trúc chứ không phải mẹo lẻ tẻ.
1. Tại sao prefetch? Mô hình “ý định người dùng”
Fetch phản ứng (reactive) là: component mount → useQuery gọi mạng → spinner → data về. Người dùng chờ toàn bộ chuỗi đó sau khi đã click. Prefetch dịch chuyển việc fetch lên trước thời điểm click, dùng các tín hiệu ý định.
REACTIVE (mặc định):
click ──▶ mount ──▶ fetch ════════▶ data ──▶ render
└─ user chờ ở đây (spinner) ─┘
PREFETCH on hover:
hover ──▶ fetch ════════▶ (data nằm sẵn trong cache)
...user còn đang rê chuột...
click ──▶ mount ──▶ useQuery đọc cache ──▶ render NGAY
└─ 0ms chờ ─┘
Khoảng thời gian giữa “hover” và “click” thường 200–500ms — đủ để một request bình thường hoàn tất. Prefetch mua khoảng trống đó.
Prefetch không làm server nhanh hơn — nó làm app cảm giác nhanh hơn. Đây là tối ưu perceived performance: ta không rút ngắn fetch, ta dịch nó ra khỏi đường tới hạn (critical path) của tương tác.
2. Bốn API prefetch — bảng tra cứu
QueryClient cung cấp một họ phương thức để nạp data ngoài render. Bảng dưới là kim chỉ nam:
| API | Trả về | Throw khi lỗi? | Dùng cache fresh? | Dùng khi |
|---|---|---|---|---|
prefetchQuery(opts) | Promise<void> | Không (nuốt lỗi) | Có | Mồi cache “best effort”, không chặn UI |
ensureQueryData(opts) | Promise<TData> | Có | Có | Cần data ngay (loader) — trả cache nếu fresh, fetch nếu chưa |
fetchQuery(opts) | Promise<TData> | Có | Có | Cần data + muốn bắt lỗi tường minh ngoài React |
prefetchInfiniteQuery(opts) | Promise<void> | Không | Có | Mồi infinite list (mục 10) |
ensureInfiniteQueryData(opts) | Promise<InfiniteData> | Có | Có | Loader cho infinite list |
Tất cả nhận cùng object options với useQuery (queryKey, queryFn, staleTime…), nên bạn truyền thẳng queryOptions đã định nghĩa cho component vào — không lặp key.
// queryOptions tái dùng cho cả prefetch lẫn useQuery (Phần 3):
function customerQuery(id: string) {
return queryOptions({
queryKey: ['customers', 'detail', id] as const,
queryFn: () => fetchCustomer(id),
staleTime: 60_000,
});
}
queryClient.prefetchQuery(customerQuery(id)); // mồi nền
const c = await queryClient.ensureQueryData(customerQuery(id)); // chờ data
Một nguồn sự thật cho key. Truyền cùng
customerQuery(id)vào loader vàuseQueryđảm bảo key trùng khít → data prefetch chính là data component đọc. Key lệch một ký tự = prefetch vô dụng (xem Gotchas, mục 12).
3. Dưới nắp: prefetch ghi thẳng vào cache
Khác biệt mấu chốt giữa prefetch và useQuery: prefetch không tạo observer. useQuery đăng ký một observer vào QueryCache (để re-render khi data đổi); prefetch chỉ kích hoạt fetch và ghi kết quả vào cache rồi thôi.
prefetchQuery(opts):
1. Tra cache theo queryKey (hash).
2. Có entry và còn FRESH (isStale === false)?
├─ Có → return ngay (KHÔNG gọi mạng). ← tôn trọng staleTime
└─ Không → gọi queryFn, ghi kết quả vào cache.
3. Resolve Promise<void>. Không observer ⇒ không
component nào re-render vì lời gọi này.
useQuery(opts) sau đó:
1. Mount → tạo observer → đọc cache.
2. Data đã nằm sẵn (do prefetch) → render NGAY, không spinner.
3. Nếu data đã stale → background refetch (vẫn show data cũ).
Hai hệ quả thực dụng:
- Prefetch lặp lại là an toàn. Hover 10 lần lên cùng link, nếu data còn fresh thì 9 lần sau là no-op. Không cần tự debounce vì
staleTime(mục 5). - Prefetch không giữ data sống. Vì không có observer, entry vẫn chịu
gcTime. Nếu user hover rồi bỏ đi quágcTime(mặc định 5 phút), data prefetch bị thu hồi — hành vi đúng, không rò bộ nhớ.
4. prefetchQuery vs ensureQueryData vs fetchQuery — chọn cái nào
Ba API trông giống nhau; khác biệt nằm ở giá trị trả về và xử lý lỗi:
Bạn có CẦN giá trị data trả về ngay không?
├─ KHÔNG → prefetchQuery (mồi nền, fire-and-forget, nuốt lỗi)
└─ CÓ
└─ Bạn có muốn lỗi THROW để xử lý (loader/error boundary)?
├─ CÓ, trong route loader → ensureQueryData (trả cache nếu fresh)
└─ CÓ, muốn LUÔN fetch tươi mới → fetchQuery (bỏ qua cache fresh? xem dưới)
Điểm tinh tế: ensureQueryData = “đảm bảo có data” → nếu cache còn fresh, nó trả luôn không fetch. fetchQuery cũng tôn trọng staleTime, nhưng ngữ nghĩa của nó là “tôi muốn fetch này”; bạn thường ép tươi mới bằng staleTime: 0 khi cần.
// (a) Mồi nền — không quan tâm kết quả/lỗi:
queryClient.prefetchQuery(customerQuery(id));
// (b) Loader — cần data, tái dùng cache fresh, throw để router bắt lỗi:
const customer = await queryClient.ensureQueryData(customerQuery(id));
// (c) Ngoài React (vd nút "Export") — cần data + bắt lỗi thủ công:
try {
const data = await queryClient.fetchQuery(reportQuery(id));
download(data);
} catch (err) {
toast.error('Tải báo cáo thất bại');
}
Quy tắc nhanh: loader dùng
ensureQueryData; “mồi link” dùngprefetchQuery; thao tác mệnh lệnh ngoài render (export, generate) dùngfetchQuery.
5. staleTime & prefetch — tránh fetch thừa
Prefetch chỉ đáng giá nếu data còn fresh lúc component thật sự mount. Đây là chỗ staleTime quyết định thành bại.
| Tình huống | staleTime | Kết quả khi click sau khi hover |
|---|---|---|
| Mặc định | 0 | Data prefetch stale ngay → useQuery refetch nền → vẫn thấy spinner-ngầm/flash |
| Hợp lý | 30_000–60_000 | Data còn fresh khi click → render tức thì, không refetch |
| Quá dài | Infinity | Render tức thì nhưng không bao giờ tự cập nhật → phải invalidate thủ công |
Với staleTime: 0 (mặc định), prefetch vẫn điền cache nên useQuery có data ngay để render, nhưng nó coi data là stale → bắn refetch nền lập tức. Bạn mất một nửa lợi ích. Đặt staleTime đủ dài để bao trọn khoảng hover→click→xem:
function customerQuery(id: string) {
return queryOptions({
queryKey: ['customers', 'detail', id] as const,
queryFn: () => fetchCustomer(id),
staleTime: 60_000, // prefetch còn giá trị trong 60s
});
}
staleTimelà “tuổi thọ” của một lần prefetch. Prefetch +staleTime: 0≈ chỉ làm ấm kết nối; muốn render thực sự tức thì, cho data một khoảng fresh.
6. Prefetch theo ý định: hover, focus, viewport
Ba tín hiệu “sắp cần data”, mỗi cái hợp một ngữ cảnh:
import { useQueryClient } from '@tanstack/react-query';
function CustomerRow({ customer }: { customer: Customer }) {
const qc = useQueryClient();
const prefetch = () => qc.prefetchQuery(customerQuery(customer.id));
return (
<Link
to={`/customers/${customer.id}`}
onMouseEnter={prefetch} // chuột: desktop
onFocus={prefetch} // bàn phím / accessibility: tab tới link
onTouchStart={prefetch} // mobile: chạm trước khi nhả
>
{customer.name}
</Link>
);
}
Đừng quên
onFocus. Nếu chỉ prefetch onmouseenter, người dùng bàn phím (và screen reader) không bao giờ kích hoạt prefetch.onFocuslàm prefetch accessible.
Prefetch khi phần tử vào viewport
Cho danh sách dài, prefetch chi tiết của item sắp lọt vào màn hình bằng IntersectionObserver:
function PrefetchOnVisible({ id }: { id: string }) {
const qc = useQueryClient();
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const obs = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
qc.prefetchQuery(customerQuery(id));
obs.disconnect(); // mồi một lần là đủ
}
},
{ rootMargin: '200px' }, // mồi trước khi thực sự thấy
);
obs.observe(el);
return () => obs.disconnect();
}, [id, qc]);
return <div ref={ref} />;
}
rootMargin: '200px' mở rộng “khung quan sát” thêm 200px ra ngoài viewport → observer báo “đã thấy” trước khi item thật sự hiện ra, cho prefetch thời gian chạy trước.
7. Chống lạm dụng: debounce & điều kiện mạng
Prefetch là tối ưu, không phải bắt buộc — đừng để nó thành DDoS chính server của mình. Vài nguyên tắc:
- Tin vào
staleTime: prefetch khi data còn fresh là no-op, nên hover lặp không tạo request mới (mục 3). - Debounce hover nếu list dày và
staleTimethấp: chỉ prefetch sau khi chuột dừng ~100ms, tránh quét chuột qua 50 dòng bắn 50 request. - Đừng prefetch trên mạng yếu: kiểm tra
navigator.connection?.saveDatahoặceffectiveTypeđể bỏ qua khi user bật tiết kiệm dữ liệu.
function shouldPrefetch(): boolean {
const conn = (navigator as Navigator & {
connection?: { saveData?: boolean; effectiveType?: string };
}).connection;
if (conn?.saveData) return false;
if (conn?.effectiveType && /2g/.test(conn.effectiveType)) return false;
return true;
}
Debounce hover bằng một timer nhỏ:
function useHoverPrefetch(run: () => void, delay = 100) {
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
const onEnter = () => {
if (!shouldPrefetch()) return;
timer.current = setTimeout(run, delay); // chỉ chạy nếu chuột nán lại
};
const onLeave = () => clearTimeout(timer.current); // rời sớm → huỷ
return { onMouseEnter: onEnter, onMouseLeave: onLeave, onFocus: run };
}
8. Route loader: triệt tiêu waterfall
Đây là nơi prefetch đáng giá nhất. Khi điều hướng bắt đầu, router có thể đồng thời tải code của route và prefetch data — thay vì “render rồi mới fetch” (waterfall).
KHÔNG loader (render-then-fetch):
navigate → tải code route ═══▶ render → useQuery fetch ═══▶ data
└─ waterfall (nối đuôi) ─┘
CÓ loader (render-as-you-fetch):
navigate ┬─ tải code route ═══▶ ┐
└─ loader: ensureQueryData ═══▶ ┴ render với data sẵn (0 spinner)
(code & data chạy SONG SONG)
React Router v7 (data mode)
import { queryClient } from '@/lib/query-client';
import { customerQuery } from '@/features/customers/queries';
import type { LoaderFunctionArgs } from 'react-router';
export async function customerLoader({ params }: LoaderFunctionArgs) {
const id = params.id;
if (!id) throw new Response('Not found', { status: 404 });
// Mồi cache; component vẫn dùng useQuery như thường.
await queryClient.ensureQueryData(customerQuery(id));
return null; // không cần trả data — nó đã nằm trong cache
}
// Component không đổi — chỉ dùng useQuery, data đã sẵn trong cache:
function CustomerDetail() {
const { id } = useParams();
const { data } = useQuery(customerQuery(id!));
return <h1>{data?.name}</h1>; // không spinner
}
Mấu chốt: loader không trả data cho component; nó chỉ điền cache. Component vẫn là một useQuery bình thường — nghĩa là vẫn được background refetch, invalidation, optimistic update… như mọi query khác. Loader chỉ là “người mồi sớm”.
TanStack Router (typed, tích hợp sẵn)
TanStack Router sinh ra để đi cùng React Query: loader và component chia sẻ context, key được type đầy đủ.
export const Route = createFileRoute('/customers/$id')({
loader: ({ context: { queryClient }, params: { id } }) =>
queryClient.ensureQueryData(customerQuery(id)),
component: CustomerDetail,
});
function CustomerDetail() {
const { id } = Route.useParams();
const { data } = useQuery(customerQuery(id)); // cùng queryOptions với loader
return <h1>{data.name}</h1>;
}
Loader nên
ensureQueryData(khôngprefetchQuery). Loader cần chờ data để router biết khi nào điều hướng xong và có thể hiển thị error boundary nếu fetch hỏng.ensureQueryDatathrow khi lỗi → router bắt được.prefetchQuerynuốt lỗi → router tưởng thành công → component render với data rỗng.
Tải nhiều resource trong một loader thì luôn song song:
loader: ({ context: { queryClient }, params: { id } }) =>
Promise.all([
queryClient.ensureQueryData(customerQuery(id)),
queryClient.ensureQueryData(customerOrdersQuery(id)),
]),
9. Render-as-you-fetch & HydrationBoundary
Pattern loader ở trên có tên: render-as-you-fetch. Triết lý: bắt đầu fetch ngay khi biết sẽ cần, không chờ component mount mới hỏi. Có ba lớp áp dụng:
| Tầng | Cơ chế | Khi nào |
|---|---|---|
| Client navigation | Route loader gọi ensureQueryData | SPA, chuyển route trong app |
| Intent | prefetchQuery on hover/focus/viewport | Trước cả khi điều hướng |
| SSR / RSC | Prefetch trên server + HydrationBoundary | Lần tải trang đầu (Phần 10) |
Với SSR (Phần 10 đã đào sâu), bạn prefetch trên server rồi dehydrate cache, gửi xuống client, và bọc cây component trong HydrationBoundary:
// Server: prefetch rồi dehydrate
const queryClient = new QueryClient();
await queryClient.prefetchQuery(customerQuery(id));
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<CustomerDetail />
</HydrationBoundary>
);
// Client: useQuery thấy data đã hydrate → render ngay, không fetch lại
function CustomerDetail() {
const { data } = useQuery(customerQuery(id)); // hit cache đã hydrate
return <h1>{data?.name}</h1>;
}
Loader + HydrationBoundary giải cùng một bài toán ở hai môi trường. Loader prefetch cho điều hướng client-side;
HydrationBoundarychuyển cache đã prefetch từ server sang client. Cả hai đều biếnuseQueryđầu tiên thành cache-hit thay vì network-fetch.
10. prefetchInfiniteQuery cho danh sách vô hạn
Infinite list (Phần 4) cũng prefetch được — kể cả nhiều trang đầu:
await queryClient.prefetchInfiniteQuery({
queryKey: customerKeys.infinite(),
queryFn: fetchCustomerPage,
initialPageParam: 0,
getNextPageParam: (last) => last.nextCursor,
// Mồi sẵn 2 trang đầu để cuộn xuống thấy ngay.
pages: 2,
});
pages: 2 bảo Query nạp trước hai trang (gọi queryFn rồi dùng getNextPageParam để biết param trang kế). Hợp khi bạn biết user gần như chắc chắn sẽ cuộn (vd feed). Trong loader, dùng ensureInfiniteQueryData để vừa mồi vừa trả InfiniteData và throw khi lỗi:
loader: ({ context: { queryClient } }) =>
queryClient.ensureInfiniteQueryData({
queryKey: customerKeys.infinite(),
queryFn: fetchCustomerPage,
initialPageParam: 0,
getNextPageParam: (last) => last.nextCursor,
}),
Đừng
pagesquá tay.pages: 5nghĩa là 5 request tuần tự (mỗi trang cần cursor của trang trước) ngay khi vào route — có thể chậm hơn cả không prefetch. Mồi 1–2 trang là đủ cho cảm giác tức thì.
11. Waterfall — nhận diện và triệt tiêu
Waterfall là khi request B chỉ bắt đầu sau khi A xong, dù B không phụ thuộc A. Dấu hiệu trong Network tab: các request xếp bậc thang thay vì song song.
WATERFALL (xấu): SONG SONG (tốt):
A ════════▶ A ════════▶
B ════════▶ B ════════▶
C═══ C ════════▶
tổng = A+B+C tổng = max(A,B,C)
Waterfall do component lồng nhau
// ❌ User load xong → mới render Posts → Posts mới fetch
function Profile() {
const { data: user } = useQuery(userQuery());
if (!user) return <Spinner />;
return <Posts userId={user.id} />; // posts chỉ fetch sau khi user xong
}
Nếu posts chỉ cần userId (mà bạn có sẵn từ route param), hãy fetch song song:
// ✅ Cả hai chạy cùng lúc
function Profile({ userId }: { userId: string }) {
const user = useQuery(userQuery(userId));
const posts = useQuery(postsQuery(userId)); // không chờ user
// ...
}
Waterfall do useSuspenseQuery lồng nhau
useSuspenseQuery suspend component cha → chặn render con → con chưa kịp gọi query. Khắc phục: gọi các suspense query cạnh nhau hoặc dùng useSuspenseQueries:
import { useSuspenseQueries } from '@tanstack/react-query';
function Dashboard() {
// Cả hai khởi chạy đồng thời, cùng suspend một lần.
const [{ data: user }, { data: stats }] = useSuspenseQueries({
queries: [userQuery(), statsQuery()],
});
return <Header user={user} stats={stats} />;
}
Waterfall do prefetch tuần tự trong loader
// ❌ chờ A rồi mới mồi B
await queryClient.ensureQueryData(aQuery());
await queryClient.ensureQueryData(bQuery());
// ✅ song song
await Promise.all([
queryClient.ensureQueryData(aQuery()),
queryClient.ensureQueryData(bQuery()),
]);
Chỉ giữ tuần tự khi thật sự phụ thuộc. Nếu
bQuerycầnidtừ kết quả củaaQuery, waterfall là bắt buộc — nhưng hãy chắc đó là phụ thuộc dữ liệu thật, không phải thói quen viếtawaitnối tiếp.
12. Gotchas thường gặp
| Vấn đề | Triệu chứng | Khắc phục |
|---|---|---|
Loader dùng prefetchQuery thay vì ensureQueryData | Lỗi bị nuốt, component render data rỗng, không vào error boundary | Dùng ensureQueryData/fetchQuery trong loader để throw |
| Key lệch giữa loader và component | Prefetch chạy nhưng useQuery vẫn spinner (cache-miss) | Tái dùng cùng queryOptions cho cả hai nơi |
staleTime: 0 (mặc định) | Prefetch xong nhưng click vẫn refetch nền | Đặt staleTime đủ dài (mục 5) |
Quên await/return trong loader | Router không chờ → render trước khi data về | await (hoặc return thẳng Promise) trong loader |
await tuần tự nhiều prefetch | Waterfall trong loader | Gói bằng Promise.all |
| Over-prefetch (mồi mọi link/mọi trang) | Mạng quá tải, lãng phí băng thông, server nóng | Debounce, tin staleTime, bỏ qua saveData, giới hạn pages |
Chỉ onMouseEnter | Người dùng bàn phím/mobile không bao giờ prefetch | Thêm onFocus + onTouchStart |
Prefetch nhưng gcTime ngắn | Data bị thu hồi trước khi click (hover sớm rồi rời lâu) | Tăng gcTime nếu khoảng hover→click có thể dài |
13. Recipes thực chiến
Link tự prefetch (hover + focus + debounce)
import { useQueryClient } from '@tanstack/react-query';
import { Link } from 'react-router';
function PrefetchLink({ id, children }: { id: string; children: React.ReactNode }) {
const qc = useQueryClient();
const prefetch = () => {
if (!shouldPrefetch()) return;
qc.prefetchQuery(customerQuery(id)); // no-op nếu còn fresh
};
return (
<Link
to={`/customers/${id}`}
onMouseEnter={prefetch}
onFocus={prefetch}
onTouchStart={prefetch}
>
{children}
</Link>
);
}
Route loader chuẩn (song song + ensureQueryData)
export const Route = createFileRoute('/customers/$id')({
loader: ({ context: { queryClient }, params: { id } }) =>
Promise.all([
queryClient.ensureQueryData(customerQuery(id)),
queryClient.ensureQueryData(customerOrdersQuery(id)),
]),
// Lỗi từ ensureQueryData ⇒ router hiển thị errorComponent.
errorComponent: ({ error }) => <ErrorPanel error={error} />,
component: CustomerDetail,
});
14. Bài tập
1. Khác biệt cốt lõi giữa prefetchQuery và ensureQueryData, và vì sao loader nên dùng ensureQueryData?
Lời giải
prefetchQuery trả Promise<void> và nuốt lỗi — hợp mồi nền best-effort. ensureQueryData trả data, dùng cache nếu còn fresh, và throw khi lỗi. Loader nên dùng ensureQueryData vì router cần chờ data và cần lỗi được throw để kích hoạt error boundary của route.
2. Vì sao nên thêm onFocus (không chỉ onMouseEnter) khi prefetch theo ý định?
Lời giải
Người dùng bàn phím và screen reader điều hướng bằng Tab, kích hoạt focus chứ không mouseenter. Nếu chỉ prefetch on hover, họ không bao giờ được hưởng lợi. Thêm onFocus (và onTouchStart cho mobile) làm prefetch bao phủ mọi cách tương tác.
3. Kể hai nguyên nhân gây request waterfall và cách triệt tiêu mỗi cái.
Lời giải
(1) Component lồng nhau khi con chỉ fetch sau khi cha có data — nâng data con lên fetch song song nếu nó không thực sự phụ thuộc cha. (2) useSuspenseQuery lồng nhau — cha suspend chặn con; dùng useSuspenseQueries hoặc đặt các query cạnh nhau. (Thêm: prefetch tuần tự trong loader — gói bằng Promise.all.)
4. Bạn prefetch trên hover nhưng khi click vẫn thấy một flash spinner ngắn. staleTime đang là mặc định. Chuyện gì xảy ra và sửa thế nào?
Lời giải
staleTime mặc định là 0, nên data prefetch stale ngay lập tức. Khi useQuery mount, nó có data trong cache (render được) nhưng coi là stale → bắn background refetch → gây flash/loading ngầm. Sửa bằng cách đặt staleTime đủ dài (vd 60_000) để data còn fresh trong khoảng hover→click→xem, khi đó useQuery không refetch.
5. Trong một loader bạn viết await queryClient.ensureQueryData(aQuery()); await queryClient.ensureQueryData(bQuery()); và route load chậm gấp đôi mong đợi. bQuery không dùng kết quả của aQuery. Sửa ra sao và vì sao nhanh hơn?
Lời giải
Hai await nối tiếp tạo waterfall: bQuery chỉ bắt đầu sau khi aQuery xong, dù không phụ thuộc. Gói bằng Promise.all([ensureQueryData(aQuery()), ensureQueryData(bQuery())]) để chạy song song → tổng thời gian là max(a, b) thay vì a + b. Chỉ giữ tuần tự khi bQuery thật sự cần dữ liệu từ aQuery.
Nâng cao: Thêm prefetch-on-viewport cho một list dài, rồi mở Network tab cuộn chậm — xác nhận chi tiết item được nạp trước khi item lọt vào màn hình, và mở trang chi tiết hiển thị không spinner. Sau đó hạ staleTime về 0 và quan sát request refetch nền xuất hiện ngay sau khi mở chi tiết.
Tóm tắt
- Prefetch = perceived performance: dịch fetch ra khỏi đường tới hạn của tương tác bằng tín hiệu ý định (hover/focus/viewport), không làm server nhanh hơn.
- Bốn API:
prefetchQuery(void, nuốt lỗi, mồi nền),ensureQueryData(trả data, dùng cache fresh, throw — cho loader),fetchQuery(data + throw, thao tác mệnh lệnh),prefetchInfiniteQuery/ensureInfiniteQueryData(infinite). Tất cả tôn trọngstaleTime. - Dưới nắp: prefetch không tạo observer — chỉ ghi cache. Lặp lại an toàn; vẫn chịu
gcTime. staleTimelà tuổi thọ của prefetch:0(mặc định) khiến click vẫn refetch nền; đặt 30–60s để render thật sự tức thì.- Route loader nạp data song song với code (render-as-you-fetch); dùng
ensureQueryDatađể router chờ và bắt lỗi (React Router v7 & TanStack Router). Component vẫn làuseQueryvới cùng queryOptions. - HydrationBoundary là phiên bản SSR của cùng ý tưởng: prefetch trên server → hydrate →
useQueryđầu tiên là cache-hit. - Waterfall đến từ component lồng, suspense lồng, và prefetch tuần tự — triệt tiêu bằng fetch song song,
useSuspenseQueries, vàPromise.all.
Phần tiếp theo
Phần 12 — QueryClient như một store: thao tác cache chủ động: dùng setQueryData/getQueriesData/setQueriesData để đọc-ghi cache trực tiếp, invalidate bằng predicate và refetchType, cancelQueries/removeQueries, rồi đăng ký queryClient.subscribe để đồng bộ chéo nhiều query mà không cần refetch.