@tanstack/react-table 9.0.0-alpha.9 → 9.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -0
- package/dist/FlexRender.cjs +61 -0
- package/dist/FlexRender.cjs.map +1 -0
- package/dist/FlexRender.d.cts +51 -0
- package/dist/FlexRender.d.ts +51 -0
- package/dist/FlexRender.js +58 -0
- package/dist/FlexRender.js.map +1 -0
- package/dist/Subscribe.cjs +13 -0
- package/dist/Subscribe.cjs.map +1 -0
- package/dist/Subscribe.d.cts +101 -0
- package/dist/Subscribe.d.ts +101 -0
- package/dist/Subscribe.js +13 -0
- package/dist/Subscribe.js.map +1 -0
- package/dist/_virtual/_rolldown/runtime.cjs +29 -0
- package/dist/createTableHook.cjs +313 -0
- package/dist/createTableHook.cjs.map +1 -0
- package/dist/createTableHook.d.cts +358 -0
- package/dist/createTableHook.d.ts +358 -0
- package/dist/createTableHook.js +311 -0
- package/dist/createTableHook.js.map +1 -0
- package/dist/flex-render.cjs +5 -0
- package/dist/flex-render.d.cts +2 -0
- package/dist/flex-render.d.ts +2 -0
- package/dist/flex-render.js +3 -0
- package/dist/index.cjs +18 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +8 -0
- package/dist/legacy.cjs +14 -0
- package/dist/legacy.d.cts +2 -0
- package/dist/legacy.d.ts +2 -0
- package/dist/legacy.js +3 -0
- package/dist/reactivity.cjs +34 -0
- package/dist/reactivity.cjs.map +1 -0
- package/dist/reactivity.js +34 -0
- package/dist/reactivity.js.map +1 -0
- package/dist/static-functions.cjs +9 -0
- package/dist/static-functions.d.cts +1 -0
- package/dist/static-functions.d.ts +1 -0
- package/dist/static-functions.js +3 -0
- package/dist/useLegacyTable.cjs +191 -0
- package/dist/useLegacyTable.cjs.map +1 -0
- package/dist/useLegacyTable.d.cts +233 -0
- package/dist/useLegacyTable.d.ts +233 -0
- package/dist/useLegacyTable.js +181 -0
- package/dist/useLegacyTable.js.map +1 -0
- package/dist/useTable.cjs +72 -0
- package/dist/useTable.cjs.map +1 -0
- package/dist/useTable.d.cts +122 -0
- package/dist/useTable.d.ts +122 -0
- package/dist/useTable.js +72 -0
- package/dist/useTable.js.map +1 -0
- package/package.json +41 -22
- package/skills/react/client-to-server/SKILL.md +377 -0
- package/skills/react/compose-with-tanstack-form/SKILL.md +363 -0
- package/skills/react/compose-with-tanstack-pacer/SKILL.md +287 -0
- package/skills/react/compose-with-tanstack-query/SKILL.md +467 -0
- package/skills/react/compose-with-tanstack-store/SKILL.md +347 -0
- package/skills/react/compose-with-tanstack-virtual/SKILL.md +388 -0
- package/skills/react/compose-with-tanstack-virtual/references/column-virtualization-and-infinite-scroll.md +136 -0
- package/skills/react/getting-started/SKILL.md +388 -0
- package/skills/react/migrate-v8-to-v9/SKILL.md +488 -0
- package/skills/react/production-readiness/SKILL.md +341 -0
- package/skills/react/react-subscribe-compiler-compat/SKILL.md +269 -0
- package/skills/react/table-state/SKILL.md +432 -0
- package/src/FlexRender.tsx +136 -0
- package/src/Subscribe.ts +153 -0
- package/src/createTableHook.tsx +1121 -0
- package/src/flex-render.ts +1 -0
- package/src/index.ts +6 -0
- package/src/legacy.ts +3 -0
- package/src/reactivity.ts +41 -0
- package/src/static-functions.ts +1 -0
- package/src/useLegacyTable.ts +487 -0
- package/src/useTable.ts +191 -0
- package/dist/cjs/index.cjs +0 -77
- package/dist/cjs/index.cjs.map +0 -1
- package/dist/cjs/index.d.cts +0 -9
- package/dist/esm/index.d.ts +0 -9
- package/dist/esm/index.js +0 -55
- package/dist/esm/index.js.map +0 -1
- package/src/index.tsx +0 -92
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react/compose-with-tanstack-virtual
|
|
3
|
+
description: >
|
|
4
|
+
`@tanstack/react-table` v9 does NOT include virtualization — pair with
|
|
5
|
+
`@tanstack/react-virtual`. Standard row-virtualization pattern: get the row
|
|
6
|
+
array from `table.getRowModel().rows`, feed `rows.length` to
|
|
7
|
+
`useVirtualizer({ count, estimateSize, getScrollElement, ... })` in the
|
|
8
|
+
DEEPEST possible component (a `TableBody`, NOT `App`), iterate
|
|
9
|
+
`rowVirtualizer.getVirtualItems()` instead of `rows.map`, absolute-position
|
|
10
|
+
each row with `transform: translateY(virtualRow.start)px`, and render
|
|
11
|
+
`<tbody>` as a CSS grid with a fixed total height. Column virtualization
|
|
12
|
+
uses `horizontal: true` plus padding-left/right placeholder cells. An
|
|
13
|
+
experimental ref-mutation variant skips React reconciliation for ~10%
|
|
14
|
+
extra perf but the standard pattern is the default.
|
|
15
|
+
type: composition
|
|
16
|
+
library: tanstack-table
|
|
17
|
+
framework: react
|
|
18
|
+
library_version: '9.0.0-alpha.48'
|
|
19
|
+
requires:
|
|
20
|
+
- react/table-state
|
|
21
|
+
- row-expanding
|
|
22
|
+
sources:
|
|
23
|
+
- TanStack/table:docs/guide/virtualization.md
|
|
24
|
+
- TanStack/table:examples/react/virtualized-rows/src/main.tsx
|
|
25
|
+
- TanStack/table:examples/react/virtualized-columns/src/main.tsx
|
|
26
|
+
- TanStack/table:examples/react/virtualized-infinite-scrolling/src/main.tsx
|
|
27
|
+
- TanStack/table:examples/react/virtualized-rows-experimental/src/main.tsx
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
This skill builds on `tanstack-table/state-management` and `tanstack-table/react/table-state`. Read those first — the table's row model is what feeds the virtualizer.
|
|
31
|
+
|
|
32
|
+
## Why this skill exists
|
|
33
|
+
|
|
34
|
+
TanStack Table renders every row in its `getRowModel().rows` array. For 50 rows that's fine; for 50k or 500k it crashes the browser. `@tanstack/react-virtual` only renders the rows that fit inside the scroll container, recycling DOM nodes as the user scrolls.
|
|
35
|
+
|
|
36
|
+
## Setup
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm add @tanstack/react-table @tanstack/react-virtual
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The two pieces:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { useTable } from '@tanstack/react-table'
|
|
46
|
+
import { useVirtualizer } from '@tanstack/react-virtual'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Core Pattern — row virtualization (standard)
|
|
50
|
+
|
|
51
|
+
The single most important rule: **keep `useVirtualizer` in the deepest component possible.** Any state change in the component that owns the virtualizer re-runs it, blowing away scroll position and measurement cache.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import * as React from 'react'
|
|
55
|
+
import {
|
|
56
|
+
useTable,
|
|
57
|
+
tableFeatures,
|
|
58
|
+
columnSizingFeature,
|
|
59
|
+
rowSortingFeature,
|
|
60
|
+
createSortedRowModel,
|
|
61
|
+
sortFns,
|
|
62
|
+
createColumnHelper,
|
|
63
|
+
} from '@tanstack/react-table'
|
|
64
|
+
import { useVirtualizer } from '@tanstack/react-virtual'
|
|
65
|
+
import type { ReactTable, Row } from '@tanstack/react-table'
|
|
66
|
+
import type { VirtualItem, Virtualizer } from '@tanstack/react-virtual'
|
|
67
|
+
|
|
68
|
+
const features = { columnSizingFeature, rowSortingFeature }
|
|
69
|
+
const columnHelper = createColumnHelper<typeof features, Person>()
|
|
70
|
+
|
|
71
|
+
const columns = columnHelper.columns([
|
|
72
|
+
columnHelper.accessor('id', { header: 'ID', size: 60 }),
|
|
73
|
+
columnHelper.accessor('firstName', { cell: (info) => info.getValue() }),
|
|
74
|
+
columnHelper.accessor('lastName', {
|
|
75
|
+
id: 'lastName',
|
|
76
|
+
cell: (info) => info.getValue(),
|
|
77
|
+
}),
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
function App() {
|
|
81
|
+
// 1) Scroll container ref + table at App level.
|
|
82
|
+
const tableContainerRef = React.useRef<HTMLDivElement>(null)
|
|
83
|
+
const [data] = React.useState(() => makeData(200_000))
|
|
84
|
+
|
|
85
|
+
const table = useTable({
|
|
86
|
+
features,
|
|
87
|
+
rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
|
|
88
|
+
columns,
|
|
89
|
+
data,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
ref={tableContainerRef}
|
|
95
|
+
style={{ overflow: 'auto', position: 'relative', height: 800 }}
|
|
96
|
+
>
|
|
97
|
+
{/* 2) display: grid — required for absolute positioning + dynamic heights */}
|
|
98
|
+
<table style={{ display: 'grid' }}>
|
|
99
|
+
<thead
|
|
100
|
+
style={{
|
|
101
|
+
display: 'grid',
|
|
102
|
+
position: 'sticky',
|
|
103
|
+
top: 0,
|
|
104
|
+
zIndex: 1,
|
|
105
|
+
height: 34,
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{table.getHeaderGroups().map((hg) => (
|
|
109
|
+
<tr
|
|
110
|
+
key={hg.id}
|
|
111
|
+
style={{ display: 'flex', height: 34, width: '100%' }}
|
|
112
|
+
>
|
|
113
|
+
{hg.headers.map((h) => (
|
|
114
|
+
<th
|
|
115
|
+
key={h.id}
|
|
116
|
+
style={{
|
|
117
|
+
display: 'flex',
|
|
118
|
+
alignItems: 'center',
|
|
119
|
+
width: h.getSize(),
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
<div onClick={h.column.getToggleSortingHandler()}>
|
|
123
|
+
<table.FlexRender header={h} />
|
|
124
|
+
</div>
|
|
125
|
+
</th>
|
|
126
|
+
))}
|
|
127
|
+
</tr>
|
|
128
|
+
))}
|
|
129
|
+
</thead>
|
|
130
|
+
{/* 3) Virtualizer lives inside TableBody, NOT here. */}
|
|
131
|
+
<TableBody table={table} tableContainerRef={tableContainerRef} />
|
|
132
|
+
</table>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface TableBodyProps {
|
|
138
|
+
table: ReactTable<typeof features, Person>
|
|
139
|
+
tableContainerRef: React.RefObject<HTMLDivElement | null>
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function TableBody({ table, tableContainerRef }: TableBodyProps) {
|
|
143
|
+
const { rows } = table.getRowModel()
|
|
144
|
+
|
|
145
|
+
// 4) useVirtualizer in the deepest body component.
|
|
146
|
+
const rowVirtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
|
|
147
|
+
count: rows.length,
|
|
148
|
+
estimateSize: () => 33,
|
|
149
|
+
getScrollElement: () => tableContainerRef.current,
|
|
150
|
+
// 5) Skip dynamic measurement on Firefox — it measures border height wrong.
|
|
151
|
+
measureElement:
|
|
152
|
+
typeof window !== 'undefined' &&
|
|
153
|
+
navigator.userAgent.indexOf('Firefox') === -1
|
|
154
|
+
? (el) => el.getBoundingClientRect().height
|
|
155
|
+
: undefined,
|
|
156
|
+
overscan: 5,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<tbody
|
|
161
|
+
style={{
|
|
162
|
+
display: 'grid',
|
|
163
|
+
height: `${rowVirtualizer.getTotalSize()}px`, // total scrollable height
|
|
164
|
+
position: 'relative', // for absolute child rows
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
|
168
|
+
const row = rows[virtualRow.index]
|
|
169
|
+
return (
|
|
170
|
+
<tr
|
|
171
|
+
key={row.id}
|
|
172
|
+
data-index={virtualRow.index}
|
|
173
|
+
ref={(node) => rowVirtualizer.measureElement(node)}
|
|
174
|
+
style={{
|
|
175
|
+
display: 'flex',
|
|
176
|
+
position: 'absolute',
|
|
177
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
178
|
+
width: '100%',
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
{row.getAllCells().map((cell) => (
|
|
182
|
+
<td
|
|
183
|
+
key={cell.id}
|
|
184
|
+
style={{ display: 'flex', width: cell.column.getSize() }}
|
|
185
|
+
>
|
|
186
|
+
<table.FlexRender cell={cell} />
|
|
187
|
+
</td>
|
|
188
|
+
))}
|
|
189
|
+
</tr>
|
|
190
|
+
)
|
|
191
|
+
})}
|
|
192
|
+
</tbody>
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Source: `examples/react/virtualized-rows/src/main.tsx`.
|
|
198
|
+
|
|
199
|
+
## Column virtualization and infinite scroll
|
|
200
|
+
|
|
201
|
+
Column virtualization (`horizontal: true` + placeholder padding cells) and infinite scroll via `useInfiniteQuery` + `manualSorting: true` — see [column-virtualization-and-infinite-scroll.md](references/column-virtualization-and-infinite-scroll.md). That file also covers the HIGH-priority `manualSorting` failure mode and the column-virt padding-cells failure mode.
|
|
202
|
+
|
|
203
|
+
## Experimental ref-mutation variant
|
|
204
|
+
|
|
205
|
+
`examples/react/virtualized-rows-experimental/` and `virtualized-columns-experimental/` mutate row `style` directly via the virtualizer's `onChange` callback, skipping React reconciliation on scroll. Roughly **10% rendering perf gain** in maintainer benchmarks. The pattern is valid but the standard pattern above is the documented default; reach for the experimental version only when measured perf demands it.
|
|
206
|
+
|
|
207
|
+
## Common Mistakes
|
|
208
|
+
|
|
209
|
+
### CRITICAL `useVirtualizer` in the same component as `useTable`
|
|
210
|
+
|
|
211
|
+
Wrong:
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
function App() {
|
|
215
|
+
const rowVirtualizer = useVirtualizer({
|
|
216
|
+
/* … */
|
|
217
|
+
}) // virtualizer too high
|
|
218
|
+
const table = useTable(opts)
|
|
219
|
+
return <TableBody table={table} virtualizer={rowVirtualizer} />
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Correct:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
function App() {
|
|
227
|
+
const tableContainerRef = React.useRef<HTMLDivElement>(null)
|
|
228
|
+
const table = useTable(opts)
|
|
229
|
+
return (
|
|
230
|
+
<div ref={tableContainerRef}>
|
|
231
|
+
<TableBody table={table} tableContainerRef={tableContainerRef} />
|
|
232
|
+
</div>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
function TableBody({ table, tableContainerRef }) {
|
|
236
|
+
const rowVirtualizer = useVirtualizer({
|
|
237
|
+
/* … */
|
|
238
|
+
}) // virtualizer deepest
|
|
239
|
+
/* … */
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Any state change in the component owning the virtualizer re-runs it — losing scroll position and remeasuring every row.
|
|
244
|
+
Source: `examples/react/virtualized-rows/src/main.tsx`.
|
|
245
|
+
|
|
246
|
+
### CRITICAL Rendering `rows.map` directly on a large dataset
|
|
247
|
+
|
|
248
|
+
Wrong:
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
<tbody>
|
|
252
|
+
{rows.map((row) => (
|
|
253
|
+
<tr key={row.id}>...</tr>
|
|
254
|
+
))}
|
|
255
|
+
</tbody>
|
|
256
|
+
// 200k DOM rows — browser crashes.
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Correct:
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
<tbody
|
|
263
|
+
style={{
|
|
264
|
+
display: 'grid',
|
|
265
|
+
height: `${rowVirtualizer.getTotalSize()}px`,
|
|
266
|
+
position: 'relative',
|
|
267
|
+
}}
|
|
268
|
+
>
|
|
269
|
+
{rowVirtualizer.getVirtualItems().map((vr) => {
|
|
270
|
+
const row = rows[vr.index]
|
|
271
|
+
return (
|
|
272
|
+
<tr
|
|
273
|
+
style={{
|
|
274
|
+
position: 'absolute',
|
|
275
|
+
transform: `translateY(${vr.start}px)` /* … */,
|
|
276
|
+
}}
|
|
277
|
+
>
|
|
278
|
+
{/* … */}
|
|
279
|
+
</tr>
|
|
280
|
+
)
|
|
281
|
+
})}
|
|
282
|
+
</tbody>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Use `getVirtualItems()` so only the visible window renders.
|
|
286
|
+
Source: `examples/react/virtualized-rows/src/main.tsx`.
|
|
287
|
+
|
|
288
|
+
### CRITICAL Missing `display: grid` + absolute positioning
|
|
289
|
+
|
|
290
|
+
Wrong:
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
<tbody>
|
|
294
|
+
{rowVirtualizer.getVirtualItems().map((vr) => (
|
|
295
|
+
<tr key={vr.key}>{/* no transform, no absolute */}</tr>
|
|
296
|
+
))}
|
|
297
|
+
</tbody>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Correct:
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
<tbody
|
|
304
|
+
style={{
|
|
305
|
+
display: 'grid',
|
|
306
|
+
height: `${rowVirtualizer.getTotalSize()}px`,
|
|
307
|
+
position: 'relative',
|
|
308
|
+
}}
|
|
309
|
+
>
|
|
310
|
+
{rowVirtualizer.getVirtualItems().map((vr) => (
|
|
311
|
+
<tr
|
|
312
|
+
style={{
|
|
313
|
+
display: 'flex',
|
|
314
|
+
position: 'absolute',
|
|
315
|
+
transform: `translateY(${vr.start}px)`,
|
|
316
|
+
width: '100%',
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
319
|
+
{/* … */}
|
|
320
|
+
</tr>
|
|
321
|
+
))}
|
|
322
|
+
</tbody>
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The semantic `<table>` layout collides with absolute positioning. CSS grid lets the rows position themselves freely while keeping semantic tags. Without `transform: translateY(start)px` all rows render at `top: 0`.
|
|
326
|
+
Source: `examples/react/virtualized-rows/src/main.tsx`.
|
|
327
|
+
|
|
328
|
+
### HIGH Using `measureElement` on Firefox
|
|
329
|
+
|
|
330
|
+
Wrong:
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
const rowVirtualizer = useVirtualizer({
|
|
334
|
+
count: rows.length,
|
|
335
|
+
estimateSize: () => 33,
|
|
336
|
+
getScrollElement: () => ref.current,
|
|
337
|
+
measureElement: (el) => el.getBoundingClientRect().height, // jitters in Firefox
|
|
338
|
+
})
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Correct:
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
const rowVirtualizer = useVirtualizer({
|
|
345
|
+
count: rows.length,
|
|
346
|
+
estimateSize: () => 33,
|
|
347
|
+
getScrollElement: () => ref.current,
|
|
348
|
+
measureElement:
|
|
349
|
+
typeof window !== 'undefined' &&
|
|
350
|
+
navigator.userAgent.indexOf('Firefox') === -1
|
|
351
|
+
? (el) => el.getBoundingClientRect().height
|
|
352
|
+
: undefined,
|
|
353
|
+
})
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Firefox returns inconsistent row heights for table rows, causing flicker. Guard the option.
|
|
357
|
+
Source: `examples/react/virtualized-rows/src/main.tsx`.
|
|
358
|
+
|
|
359
|
+
### HIGH Storing the ref instead of using the callback-ref form
|
|
360
|
+
|
|
361
|
+
Wrong:
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
const rowRef = React.useRef(null)
|
|
365
|
+
<tr ref={rowRef} /*…*/ />
|
|
366
|
+
// rowVirtualizer can't remeasure when row content changes height
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Correct:
|
|
370
|
+
|
|
371
|
+
```tsx
|
|
372
|
+
<tr ref={(node) => rowVirtualizer.measureElement(node)} /*…*/ />
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
The pattern is a ref callback that calls `measureElement(node)` — passing a stored ref means the virtualizer never gets a chance to remeasure.
|
|
376
|
+
Source: `examples/react/virtualized-rows/src/main.tsx`.
|
|
377
|
+
|
|
378
|
+
For HIGH-priority failure modes specific to column virtualization (missing padding placeholders) and infinite scroll (`manualSorting` requirement), see [column-virtualization-and-infinite-scroll.md](references/column-virtualization-and-infinite-scroll.md).
|
|
379
|
+
|
|
380
|
+
## See Also
|
|
381
|
+
|
|
382
|
+
- `tanstack-table/react/production-readiness` — keep virtualizers in deepest components.
|
|
383
|
+
- `tanstack-table/react/compose-with-tanstack-query` — `useInfiniteQuery` integration.
|
|
384
|
+
- `tanstack-table/react/table-state` — the row model API the virtualizer reads from.
|
|
385
|
+
|
|
386
|
+
## References
|
|
387
|
+
|
|
388
|
+
- [column-virtualization-and-infinite-scroll.md](references/column-virtualization-and-infinite-scroll.md) — `horizontal: true` column virtualization with placeholder padding cells, `useInfiniteQuery` + `manualSorting: true` integration, plus HIGH-priority failure modes for both
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Column virtualization and infinite scroll — React + TanStack Virtual
|
|
2
|
+
|
|
3
|
+
Extended composition patterns extracted from `SKILL.md`. The primary row-virtualization pattern and the experimental ref-mutation variant remain inline in the SKILL; this file covers column virtualization and the `useInfiniteQuery` integration.
|
|
4
|
+
|
|
5
|
+
## Column virtualization
|
|
6
|
+
|
|
7
|
+
Same shape as row virtualization with `horizontal: true` and left/right placeholder cells so unrendered columns still take up scroll width:
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
const columnVirtualizer = useVirtualizer({
|
|
11
|
+
count: table.getVisibleLeafColumns().length,
|
|
12
|
+
estimateSize: (i) => table.getVisibleLeafColumns()[i].getSize(),
|
|
13
|
+
getScrollElement: () => tableContainerRef.current,
|
|
14
|
+
horizontal: true,
|
|
15
|
+
overscan: 3,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const virtualColumns = columnVirtualizer.getVirtualItems()
|
|
19
|
+
const virtualPaddingLeft = virtualColumns[0]?.start ?? 0
|
|
20
|
+
const virtualPaddingRight =
|
|
21
|
+
columnVirtualizer.getTotalSize() -
|
|
22
|
+
(virtualColumns[virtualColumns.length - 1]?.end ?? 0)
|
|
23
|
+
|
|
24
|
+
// In each row:
|
|
25
|
+
<tr>
|
|
26
|
+
{virtualPaddingLeft > 0 ? <td style={{ width: virtualPaddingLeft }} /> : null}
|
|
27
|
+
{virtualColumns.map((vc) => {
|
|
28
|
+
const cell = row.getVisibleCells()[vc.index]
|
|
29
|
+
return <td key={cell.id}><table.FlexRender cell={cell} /></td>
|
|
30
|
+
})}
|
|
31
|
+
{virtualPaddingRight > 0 ? <td style={{ width: virtualPaddingRight }} /> : null}
|
|
32
|
+
</tr>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Source: `examples/react/virtualized-columns/src/main.tsx`.
|
|
36
|
+
|
|
37
|
+
## Infinite scroll — Virtual + `useInfiniteQuery`
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
const dataQuery = useInfiniteQuery({
|
|
41
|
+
queryKey: ['people', sorting],
|
|
42
|
+
queryFn: ({ pageParam = 0 }) => fetchPage(pageParam, sorting),
|
|
43
|
+
getNextPageParam: (lastPage, allPages) => allPages.length,
|
|
44
|
+
placeholderData: keepPreviousData,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const flatRows = React.useMemo(
|
|
48
|
+
() => dataQuery.data?.pages.flatMap((p) => p.rows) ?? [],
|
|
49
|
+
[dataQuery.data],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const table = useTable({
|
|
53
|
+
features: tableFeatures({ rowSortingFeature }),
|
|
54
|
+
rowModels: {}, // server sorts each page
|
|
55
|
+
columns,
|
|
56
|
+
data: flatRows,
|
|
57
|
+
manualSorting: true,
|
|
58
|
+
// ...
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Inside TableBody, scroll handler:
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
const el = tableContainerRef.current
|
|
64
|
+
if (!el) return
|
|
65
|
+
const onScroll = () => {
|
|
66
|
+
if (
|
|
67
|
+
el.scrollHeight - el.scrollTop - el.clientHeight < 500 &&
|
|
68
|
+
!dataQuery.isFetching
|
|
69
|
+
) {
|
|
70
|
+
dataQuery.fetchNextPage()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
el.addEventListener('scroll', onScroll)
|
|
74
|
+
return () => el.removeEventListener('scroll', onScroll)
|
|
75
|
+
}, [dataQuery])
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Source: `examples/react/virtualized-infinite-scrolling/src/main.tsx`.
|
|
79
|
+
|
|
80
|
+
## Common Mistakes (column virt + infinite scroll)
|
|
81
|
+
|
|
82
|
+
### HIGH For column virtualization: missing padding placeholder cells
|
|
83
|
+
|
|
84
|
+
Wrong:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<tr>
|
|
88
|
+
{virtualColumns.map((vc) => (
|
|
89
|
+
<td key={vc.index}>...</td>
|
|
90
|
+
))}
|
|
91
|
+
</tr>
|
|
92
|
+
// Unrendered columns aren't taking up scroll space → visible columns slide left.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Correct:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
<tr>
|
|
99
|
+
{virtualPaddingLeft > 0 ? <td style={{ width: virtualPaddingLeft }} /> : null}
|
|
100
|
+
{virtualColumns.map((vc) => (
|
|
101
|
+
<td key={vc.index}>...</td>
|
|
102
|
+
))}
|
|
103
|
+
{virtualPaddingRight > 0 ? (
|
|
104
|
+
<td style={{ width: virtualPaddingRight }} />
|
|
105
|
+
) : null}
|
|
106
|
+
</tr>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Source: `examples/react/virtualized-columns/src/main.tsx`.
|
|
110
|
+
|
|
111
|
+
### HIGH Infinite scroll without `manualSorting`
|
|
112
|
+
|
|
113
|
+
Wrong:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
const table = useTable({
|
|
117
|
+
features: tableFeatures({ rowSortingFeature }),
|
|
118
|
+
rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
|
|
119
|
+
data: flatRows,
|
|
120
|
+
})
|
|
121
|
+
// Each new page arrives → table re-sorts everything → row order scrambles between pages.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Correct:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
const table = useTable({
|
|
128
|
+
features: tableFeatures({ rowSortingFeature }),
|
|
129
|
+
rowModels: {}, // server sorts each page
|
|
130
|
+
data: flatRows,
|
|
131
|
+
manualSorting: true,
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
With `useInfiniteQuery`, you must fire a fresh query on sort changes (key your `queryKey` on `sorting`) and set `manualSorting: true` so the table doesn't re-sort accumulated pages.
|
|
136
|
+
Source: `examples/react/virtualized-infinite-scrolling/src/main.tsx`.
|