Plain fetcher
useDataGridLazyRows with a plain simulated-latency fetcher, as the baseline pattern.
The baseline: useDataGridLazyRows owns the sparse row array and the range bookkeeping.
You own fetchRows(start, end, signal). The demo has 100,000 rows. Only the ranges that
actually scroll into view are ever fetched, and skeleton rows hold the space until each
range lands. The two failure buttons show the transient/permanent error split. The third
page covers what a real fetcher replaces.
0 / 100,000 rows loaded
pnpm dlx shadcn@latest add @gridcn/data-grid-lazy-demo
The seam
const lazy = useDataGridLazyRows<Row>({
total: 100_000,
getRowId: (row) => row.id,
fetchRows: (start, end, signal) => api.fetchRows(start, end, signal),
onError: (error, range) => {
/* transient: auto-retries on next scroll; permanent: surface a retry button */
},
});fetchRowsresolves exactly the[start, end)slice, positionally aligned tostart. The hook writes it into the sparse array. The core renders real rows over the skeletons.signalaborts the in-flight fetch when the range leaves view. A rejection after abort is ignored, so an abort is not an error. Reject with anAbortErrorand move on.- Failed ranges revert to unloaded and the next scroll into them retries automatically
("refetch on next visibility").
onErrorstill fires, so a permanent failure (an expired token, for example) can surface a manual retry. Re-firegridProps.onRenderedRowsChange(range), and the range takes the same path as a real scroll.
Bring your own fetcher
The two library pages wire the same seam to a cache so fetched ranges survive the rest of the session: TanStack Query and SWR.
Behavior reference
- Lazy loading: overscan, batch size, and the uncontrolled sort/filter/search guard
- Tuning the fetch range:
maxFetchRowsand the other size knobs - Memory & retention:
reset,evict, andgetLoadedRanges
Related
- Large data: the opposite pattern: everything loaded, virtualization does the work