TanStack Query
Lazy ranges through queryClient.fetchQuery, which dedupes requests and caches each range for the session, so scrolling back never refetches.
Each scrolled range becomes a fetchQuery: TanStack Query dedupes concurrent ranges,
keeps every fetched range in the cache (staleTime: Infinity), and the hook's abort signal
still cancels the network call when a range leaves view. Scrolling back never refetches.
The "ranges in the query cache" counter in the demo bar makes that visible.
pnpm dlx shadcn@latest add @gridcn/data-grid-lazy-react-query-demo
The seam
const queryClient = useQueryClient();
const lazy = useDataGridLazyRows<Row>({
total: 10_000,
getRowId: (row) => row.id,
fetchRows: (start, end, signal) =>
queryClient.fetchQuery({
queryKey: ["rows", start, end],
staleTime: Infinity,
queryFn: () => api.fetchRows(start, end, signal),
}),
});The flow in words: viewport → range ([start, end)) → query key → sparse rows. The query
key is the range, so two overlapping scrolls to the same range share one fetch, and
the cache entry survives for the session. staleTime: Infinity is what keeps a revisited
range from refetching, because the default (0) marks data stale immediately.
Why not useInfiniteQuery
useInfiniteQuery fetches pages sequentially from page 0. A virtualized grid asks for
arbitrary ranges. Row 9,000 before row 50 exists. So the imperative fetchQuery is the
fitting seam: same cache, same dedupe, no page-ordering constraint.
Behavior reference
- Lazy loading: the
useDataGridLazyRowscontract
Related
- Plain fetcher: the baseline without a cache
- SWR: the same pattern through SWR's global cache