@tanstack/preact-table 9.0.0-alpha.47 → 9.0.0-alpha.49
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 +10 -0
- package/package.json +7 -5
- package/skills/preact/client-to-server/SKILL.md +294 -0
- package/skills/preact/compose-with-tanstack-form/SKILL.md +230 -0
- package/skills/preact/compose-with-tanstack-pacer/SKILL.md +186 -0
- package/skills/preact/compose-with-tanstack-query/SKILL.md +283 -0
- package/skills/preact/compose-with-tanstack-store/SKILL.md +263 -0
- package/skills/preact/compose-with-tanstack-virtual/SKILL.md +275 -0
- package/skills/preact/getting-started/SKILL.md +371 -0
- package/skills/preact/migrate-v8-to-v9/SKILL.md +322 -0
- package/skills/preact/production-readiness/SKILL.md +278 -0
- package/skills/preact/table-state/SKILL.md +432 -0
- package/skills/preact/table-state/references/advanced-state-patterns.md +93 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: preact/compose-with-tanstack-store
|
|
3
|
+
description: >
|
|
4
|
+
`@tanstack/preact-table` v9 is built on TanStack Store. Each registered state
|
|
5
|
+
slice is an atom. The table exposes three reactive surfaces:
|
|
6
|
+
`table.atoms.<slice>` (per-slice readonly), `table.store` (flat readonly
|
|
7
|
+
view), and `table.baseAtoms.<slice>` (internal writable). Use external atoms
|
|
8
|
+
via `useCreateAtom` + `options.atoms` to hand slice ownership to your app,
|
|
9
|
+
share atoms across components with `useSelector`, and subscribe imperatively
|
|
10
|
+
with `atom.subscribe(...)` for persistence/sync. Routing keywords:
|
|
11
|
+
preact-store, useCreateAtom, useSelector, atoms, external state, slice
|
|
12
|
+
ownership, persistence.
|
|
13
|
+
type: composition
|
|
14
|
+
library: tanstack-table
|
|
15
|
+
framework: preact
|
|
16
|
+
library_version: '9.0.0-alpha.48'
|
|
17
|
+
requires:
|
|
18
|
+
- state-management
|
|
19
|
+
- preact/table-state
|
|
20
|
+
sources:
|
|
21
|
+
- TanStack/table:docs/framework/preact/guide/table-state.md
|
|
22
|
+
- TanStack/table:examples/preact/basic-external-atoms/src/main.tsx
|
|
23
|
+
- TanStack/table:examples/preact/basic-subscribe/src/main.tsx
|
|
24
|
+
- TanStack/table:packages/preact-table/src/useTable.ts
|
|
25
|
+
- TanStack/table:packages/preact-table/src/reactivity.ts
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
`@tanstack/preact-table` v9 stores every registered state slice as a TanStack Store atom under the hood, and `useTable` wires Preact to those atoms via `@tanstack/preact-store`. This skill is the bridge between table state and the rest of your TanStack Store-powered app.
|
|
29
|
+
|
|
30
|
+
## The Three Surfaces
|
|
31
|
+
|
|
32
|
+
| Surface | Shape | Use when |
|
|
33
|
+
| ------------------------- | ---------------------------------------------------------- | --------------------------------------------------- |
|
|
34
|
+
| `table.atoms.<slice>` | `ReadonlyAtom<TSliceState>` per slice | Read or subscribe to one slice (preferred) |
|
|
35
|
+
| `table.store` | `ReadonlyStore<TableState<TFeatures>>` (flat derived view) | Reading full table state, selecting projections |
|
|
36
|
+
| `table.baseAtoms.<slice>` | `Atom<TSliceState>` (writable internal) | Low-level writes when the slice is internally owned |
|
|
37
|
+
|
|
38
|
+
External atoms passed via `options.atoms.<slice>` take precedence over `options.state[<slice>]` and over `table.baseAtoms.<slice>`. Writes from feature APIs (`table.setSorting(...)`, `table.setPageIndex(...)`, etc.) flow into whichever atom owns the slice.
|
|
39
|
+
|
|
40
|
+
Source: `docs/framework/preact/guide/table-state.md`; `packages/preact-table/src/useTable.ts`.
|
|
41
|
+
|
|
42
|
+
## Pattern 1 — Hand a slice to your app
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { useCreateAtom, useSelector } from '@tanstack/preact-store'
|
|
46
|
+
import {
|
|
47
|
+
rowPaginationFeature,
|
|
48
|
+
rowSortingFeature,
|
|
49
|
+
tableFeatures,
|
|
50
|
+
useTable,
|
|
51
|
+
type PaginationState,
|
|
52
|
+
type SortingState,
|
|
53
|
+
} from '@tanstack/preact-table'
|
|
54
|
+
|
|
55
|
+
const _features = tableFeatures({ rowPaginationFeature, rowSortingFeature })
|
|
56
|
+
|
|
57
|
+
function PeopleTable({ data }) {
|
|
58
|
+
// Stable atoms owned by this component.
|
|
59
|
+
const sortingAtom = useCreateAtom<SortingState>([])
|
|
60
|
+
const paginationAtom = useCreateAtom<PaginationState>({
|
|
61
|
+
pageIndex: 0,
|
|
62
|
+
pageSize: 10,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Independent reactive reads — only re-renders for the slice that changed.
|
|
66
|
+
const sorting = useSelector(sortingAtom)
|
|
67
|
+
const pagination = useSelector(paginationAtom)
|
|
68
|
+
|
|
69
|
+
const table = useTable({
|
|
70
|
+
_features,
|
|
71
|
+
_rowModels: {
|
|
72
|
+
/* … */
|
|
73
|
+
},
|
|
74
|
+
columns,
|
|
75
|
+
data,
|
|
76
|
+
atoms: { sorting: sortingAtom, pagination: paginationAtom },
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// table.setSorting / table.setPageIndex write to your atoms.
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Source: `examples/preact/basic-external-atoms/src/main.tsx`.
|
|
85
|
+
|
|
86
|
+
## Pattern 2 — Share an atom across components
|
|
87
|
+
|
|
88
|
+
Lift the atom to a module / context. Any component can read or write it, and the table stays in sync.
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
// shared/atoms.ts
|
|
92
|
+
import { createAtom } from '@tanstack/preact-store'
|
|
93
|
+
import type { RowSelectionState } from '@tanstack/preact-table'
|
|
94
|
+
|
|
95
|
+
export const selectionAtom = createAtom<RowSelectionState>({})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
// TableScreen.tsx
|
|
100
|
+
import { selectionAtom } from '../shared/atoms'
|
|
101
|
+
|
|
102
|
+
const table = useTable({
|
|
103
|
+
_features,
|
|
104
|
+
_rowModels: {},
|
|
105
|
+
columns,
|
|
106
|
+
data,
|
|
107
|
+
atoms: { rowSelection: selectionAtom },
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
// SelectionSummary.tsx
|
|
113
|
+
import { useSelector } from '@tanstack/preact-store'
|
|
114
|
+
import { selectionAtom } from '../shared/atoms'
|
|
115
|
+
|
|
116
|
+
function SelectionSummary() {
|
|
117
|
+
const sel = useSelector(selectionAtom)
|
|
118
|
+
return <span>{Object.keys(sel).length} selected</span>
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Module-scope atoms are stable identities — no `useCreateAtom` needed. Don't create module-scope atoms inside a component-render body.
|
|
123
|
+
|
|
124
|
+
Source: `docs/framework/preact/guide/table-state.md`.
|
|
125
|
+
|
|
126
|
+
## Pattern 3 — Subscribe imperatively for persistence / sync
|
|
127
|
+
|
|
128
|
+
`Atom.subscribe` returns an `{ unsubscribe }` handle. Persist to `localStorage`, push to a URL, or fan out to other stores.
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { useEffect } from 'preact/hooks'
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const sub = paginationAtom.subscribe((next) => {
|
|
135
|
+
localStorage.setItem('table:pagination', JSON.stringify(next))
|
|
136
|
+
})
|
|
137
|
+
return () => sub.unsubscribe()
|
|
138
|
+
}, [paginationAtom])
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
You can also subscribe to `table.atoms.<slice>` directly without owning the slice. The subscription fires whenever the slice changes — whoever owns it (your atom or `baseAtoms`).
|
|
142
|
+
|
|
143
|
+
Source: `packages/preact-table/src/reactivity.ts`.
|
|
144
|
+
|
|
145
|
+
## Pattern 4 — Read inside cells with the standalone `<Subscribe>`
|
|
146
|
+
|
|
147
|
+
Inside a cell or header render context, `table` is the core `Table<TFeatures, TData>`, not `PreactTable`. `table.Subscribe` is undefined — import the standalone component.
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
import { Subscribe } from '@tanstack/preact-table'
|
|
151
|
+
|
|
152
|
+
columnHelper.display({
|
|
153
|
+
id: 'select',
|
|
154
|
+
cell: ({ row, table }) => (
|
|
155
|
+
<Subscribe source={table.atoms.rowSelection} selector={(rs) => rs[row.id]}>
|
|
156
|
+
{(isSelected) => (
|
|
157
|
+
<input
|
|
158
|
+
type="checkbox"
|
|
159
|
+
checked={!!isSelected}
|
|
160
|
+
onChange={row.getToggleSelectedHandler()}
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
163
|
+
</Subscribe>
|
|
164
|
+
),
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Source: `packages/preact-table/src/Subscribe.ts`; `examples/preact/basic-subscribe/src/main.tsx`.
|
|
169
|
+
|
|
170
|
+
## Common Mistakes
|
|
171
|
+
|
|
172
|
+
### CRITICAL Creating an atom inside the render body without `useCreateAtom`
|
|
173
|
+
|
|
174
|
+
Wrong:
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
function MyTable() {
|
|
178
|
+
const sortingAtom = createAtom<SortingState>([]) // new atom every render
|
|
179
|
+
useTable({ /* … */, atoms: { sorting: sortingAtom } })
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Correct:
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
function MyTable() {
|
|
187
|
+
const sortingAtom = useCreateAtom<SortingState>([]) // stable
|
|
188
|
+
useTable({ /* … */, atoms: { sorting: sortingAtom } })
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
A fresh atom each render unbinds the slice and resets it to the initial value on every render.
|
|
193
|
+
Source: `examples/preact/basic-external-atoms/src/main.tsx`.
|
|
194
|
+
|
|
195
|
+
### HIGH Writing to `table.baseAtoms.X.set()` when the slice is externally owned
|
|
196
|
+
|
|
197
|
+
Wrong:
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
useTable({ /* … */, atoms: { pagination: paginationAtom } })
|
|
201
|
+
table.baseAtoms.pagination.set((old) => ({ ...old, pageIndex: 0 })) // updates the wrong atom
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Correct:
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
// Use the feature API (writes to whichever atom owns the slice).
|
|
208
|
+
table.setPageIndex(0)
|
|
209
|
+
// Or write to your external atom directly.
|
|
210
|
+
paginationAtom.set((old) => ({ ...old, pageIndex: 0 }))
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`table.baseAtoms` is the internal writable atom — used only when the slice is internally owned. When you hand a slice to an external atom, the external atom is the source of truth.
|
|
214
|
+
Source: `docs/framework/preact/guide/table-state.md`.
|
|
215
|
+
|
|
216
|
+
### HIGH Reading `.get()` and expecting re-renders
|
|
217
|
+
|
|
218
|
+
Wrong:
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
function Pager() {
|
|
222
|
+
const { pageIndex } = table.atoms.pagination.get() // current-value read
|
|
223
|
+
return <span>Page {pageIndex + 1}</span>
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Correct:
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
import { useSelector } from '@tanstack/preact-store'
|
|
231
|
+
|
|
232
|
+
function Pager() {
|
|
233
|
+
const pageIndex = useSelector(table.atoms.pagination, (p) => p.pageIndex)
|
|
234
|
+
return <span>Page {pageIndex + 1}</span>
|
|
235
|
+
}
|
|
236
|
+
// or
|
|
237
|
+
;<table.Subscribe source={table.atoms.pagination} selector={(p) => p.pageIndex}>
|
|
238
|
+
{(pageIndex) => <span>Page {pageIndex + 1}</span>}
|
|
239
|
+
</table.Subscribe>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`.get()` returns the current value without subscribing.
|
|
243
|
+
|
|
244
|
+
### MEDIUM Passing the same slice via `atoms` AND `state`
|
|
245
|
+
|
|
246
|
+
Wrong:
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
useTable({
|
|
250
|
+
/* … */,
|
|
251
|
+
atoms: { pagination: paginationAtom },
|
|
252
|
+
state: { pagination }, // silently ignored
|
|
253
|
+
onPaginationChange: setPagination, // silently ignored
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Correct: pick exactly one ownership path per slice.
|
|
258
|
+
|
|
259
|
+
## See Also
|
|
260
|
+
|
|
261
|
+
- `tanstack-table/preact/table-state` — atoms / Subscribe / FlexRender reference.
|
|
262
|
+
- `tanstack-table/preact/compose-with-tanstack-query` — server-side flow keyed by atoms.
|
|
263
|
+
- `tanstack-table/preact/production-readiness` — when to reach for narrow subscriptions.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: preact/compose-with-tanstack-virtual
|
|
3
|
+
description: >
|
|
4
|
+
TanStack Table does NOT include virtualization — pair with TanStack Virtual.
|
|
5
|
+
Preact has no dedicated `@tanstack/preact-virtual` adapter yet; use
|
|
6
|
+
`@tanstack/virtual-core`'s `Virtualizer` class behind a small hook, or use
|
|
7
|
+
the React adapter via `preact/compat`. Pattern: get `rows = table.getRowModel().rows`,
|
|
8
|
+
feed `rows.length` to the virtualizer, render only virtual items, and use
|
|
9
|
+
CSS transforms for row positioning. Routing keywords: preact virtualization,
|
|
10
|
+
large table, virtual rows, virtual-core, getVirtualItems, table-core.
|
|
11
|
+
type: composition
|
|
12
|
+
library: tanstack-table
|
|
13
|
+
framework: preact
|
|
14
|
+
library_version: '9.0.0-alpha.48'
|
|
15
|
+
requires:
|
|
16
|
+
- preact/table-state
|
|
17
|
+
- row-expanding
|
|
18
|
+
sources:
|
|
19
|
+
- TanStack/table:docs/guide/virtualization.md
|
|
20
|
+
- TanStack/table:examples/lit/virtualized-rows/src/main.ts
|
|
21
|
+
- TanStack/table:examples/react/virtualized-rows/
|
|
22
|
+
- TanStack/table:examples/react/virtualized-columns/
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
TanStack Table is headless — it does not virtualize rows or columns. For long lists, pair the table with TanStack Virtual.
|
|
26
|
+
|
|
27
|
+
> **Adapter status:** There is no published `@tanstack/preact-virtual` adapter as of `@tanstack/table` v9.0.0-alpha.48. The two supported paths are:
|
|
28
|
+
>
|
|
29
|
+
> 1. **Use `@tanstack/virtual-core` directly.** The framework-agnostic `Virtualizer` class wrapped in a small Preact hook is the recommended path.
|
|
30
|
+
> 2. **Use `@tanstack/react-virtual` via `preact/compat`.** Works if your project already aliases `react` → `preact/compat`.
|
|
31
|
+
>
|
|
32
|
+
> The patterns below use path 1. Both paths feed the same `rows = table.getRowModel().rows` array to the virtualizer.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @tanstack/virtual-core
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## The Pattern (Row Virtualization)
|
|
41
|
+
|
|
42
|
+
1. Build the table with `useTable` as usual.
|
|
43
|
+
2. Get `const { rows } = table.getRowModel()` — the table is the source of truth for which rows to render (already sorted, filtered, paginated, etc.).
|
|
44
|
+
3. Pass `rows.length` to the virtualizer.
|
|
45
|
+
4. Render only `virtualizer.getVirtualItems()`.
|
|
46
|
+
5. Each virtual row is absolutely positioned via `transform: translateY(${item.start}px)`.
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
|
50
|
+
import {
|
|
51
|
+
Virtualizer,
|
|
52
|
+
observeElementOffset,
|
|
53
|
+
observeElementRect,
|
|
54
|
+
elementScroll,
|
|
55
|
+
} from '@tanstack/virtual-core'
|
|
56
|
+
import {
|
|
57
|
+
useTable,
|
|
58
|
+
createSortedRowModel,
|
|
59
|
+
rowSortingFeature,
|
|
60
|
+
sortFns,
|
|
61
|
+
tableFeatures,
|
|
62
|
+
} from '@tanstack/preact-table'
|
|
63
|
+
import type { JSX } from 'preact'
|
|
64
|
+
|
|
65
|
+
const _features = tableFeatures({ rowSortingFeature })
|
|
66
|
+
|
|
67
|
+
// Minimal Preact hook around the framework-agnostic Virtualizer.
|
|
68
|
+
function useVirtualizer<TScrollEl extends Element, TItemEl extends Element>(
|
|
69
|
+
options: Omit<
|
|
70
|
+
ConstructorParameters<typeof Virtualizer<TScrollEl, TItemEl>>[0],
|
|
71
|
+
'observeElementRect' | 'observeElementOffset' | 'scrollToFn'
|
|
72
|
+
> & { onChange?: (instance: Virtualizer<TScrollEl, TItemEl>) => void },
|
|
73
|
+
) {
|
|
74
|
+
const [, force] = useState(0)
|
|
75
|
+
const virtualizer = useMemo(
|
|
76
|
+
() =>
|
|
77
|
+
new Virtualizer<TScrollEl, TItemEl>({
|
|
78
|
+
...options,
|
|
79
|
+
observeElementRect,
|
|
80
|
+
observeElementOffset,
|
|
81
|
+
scrollToFn: elementScroll,
|
|
82
|
+
onChange: (inst) => {
|
|
83
|
+
options.onChange?.(inst)
|
|
84
|
+
force((n) => n + 1)
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
[],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
// Sync count / estimateSize / overscan when they change.
|
|
91
|
+
virtualizer.setOptions({
|
|
92
|
+
...virtualizer.options,
|
|
93
|
+
count: options.count,
|
|
94
|
+
estimateSize: options.estimateSize,
|
|
95
|
+
overscan: options.overscan,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
return virtualizer._didMount()
|
|
100
|
+
}, [virtualizer])
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
virtualizer._willUpdate()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return virtualizer
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function BigTable({ data }) {
|
|
110
|
+
const table = useTable(
|
|
111
|
+
{
|
|
112
|
+
_features,
|
|
113
|
+
_rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
|
|
114
|
+
columns,
|
|
115
|
+
data,
|
|
116
|
+
},
|
|
117
|
+
() => null, // huge table — opt out at the top, subscribe lower down
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const { rows } = table.getRowModel()
|
|
121
|
+
|
|
122
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
123
|
+
|
|
124
|
+
const rowVirtualizer = useVirtualizer({
|
|
125
|
+
count: rows.length,
|
|
126
|
+
getScrollElement: () => scrollRef.current!,
|
|
127
|
+
estimateSize: () => 33,
|
|
128
|
+
overscan: 5,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div
|
|
133
|
+
ref={scrollRef}
|
|
134
|
+
style={{ overflow: 'auto', height: 800, position: 'relative' }}
|
|
135
|
+
>
|
|
136
|
+
<table style={{ display: 'grid' }}>
|
|
137
|
+
<thead
|
|
138
|
+
style={{ display: 'grid', position: 'sticky', top: 0, zIndex: 1 }}
|
|
139
|
+
>
|
|
140
|
+
{table.getHeaderGroups().map((hg) => (
|
|
141
|
+
<tr key={hg.id} style={{ display: 'flex', width: '100%' }}>
|
|
142
|
+
{hg.headers.map((h) => (
|
|
143
|
+
<th key={h.id} style={{ display: 'flex', width: h.getSize() }}>
|
|
144
|
+
<table.FlexRender header={h} />
|
|
145
|
+
</th>
|
|
146
|
+
))}
|
|
147
|
+
</tr>
|
|
148
|
+
))}
|
|
149
|
+
</thead>
|
|
150
|
+
|
|
151
|
+
<tbody
|
|
152
|
+
style={{
|
|
153
|
+
display: 'grid',
|
|
154
|
+
height: `${rowVirtualizer.getTotalSize()}px`,
|
|
155
|
+
position: 'relative',
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
{rowVirtualizer.getVirtualItems().map((virtualItem) => {
|
|
159
|
+
const row = rows[virtualItem.index]
|
|
160
|
+
return (
|
|
161
|
+
<tr
|
|
162
|
+
key={row.id}
|
|
163
|
+
ref={(node) => rowVirtualizer.measureElement(node!)}
|
|
164
|
+
data-index={virtualItem.index}
|
|
165
|
+
style={{
|
|
166
|
+
display: 'flex',
|
|
167
|
+
position: 'absolute',
|
|
168
|
+
transform: `translateY(${virtualItem.start}px)`,
|
|
169
|
+
width: '100%',
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
{row.getAllCells().map((cell) => (
|
|
173
|
+
<td
|
|
174
|
+
key={cell.id}
|
|
175
|
+
style={{ display: 'flex', width: cell.column.getSize() }}
|
|
176
|
+
>
|
|
177
|
+
<table.FlexRender cell={cell} />
|
|
178
|
+
</td>
|
|
179
|
+
))}
|
|
180
|
+
</tr>
|
|
181
|
+
)
|
|
182
|
+
})}
|
|
183
|
+
</tbody>
|
|
184
|
+
</table>
|
|
185
|
+
</div>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The structure matches the Lit virtualized-rows example one-for-one; only the host framework changes.
|
|
191
|
+
Source: `examples/lit/virtualized-rows/src/main.ts`; `docs/guide/virtualization.md`.
|
|
192
|
+
|
|
193
|
+
## Column Virtualization
|
|
194
|
+
|
|
195
|
+
Same idea, but the virtualizer's `count` is `columns.length` and you index the visible columns inside each row. Useful for wide kitchen-sink tables.
|
|
196
|
+
|
|
197
|
+
## With Pagination / Filtering
|
|
198
|
+
|
|
199
|
+
Use the row model that's already been transformed by registered features. The virtualizer count is `rows.length` — the table handles sorting, filtering, and pagination upstream.
|
|
200
|
+
|
|
201
|
+
## Combining with `<table.Subscribe>`
|
|
202
|
+
|
|
203
|
+
On large tables, pass `() => null` to `useTable` (or use the standalone `<Subscribe>`) and wrap the `<tbody>` in a subscription that re-renders only when the row model can actually change.
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
<table.Subscribe
|
|
207
|
+
selector={(s) => ({
|
|
208
|
+
columnFilters: s.columnFilters,
|
|
209
|
+
globalFilter: s.globalFilter,
|
|
210
|
+
sorting: s.sorting,
|
|
211
|
+
})}
|
|
212
|
+
>
|
|
213
|
+
{() => <tbody>{/* virtualized rows */}</tbody>}
|
|
214
|
+
</table.Subscribe>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Source: `examples/preact/basic-subscribe/src/main.tsx`.
|
|
218
|
+
|
|
219
|
+
## Common Mistakes
|
|
220
|
+
|
|
221
|
+
### CRITICAL Reimplementing virtualization by hand
|
|
222
|
+
|
|
223
|
+
Wrong:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
// Manual slicing + intersection observer + per-row offset calculation
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Correct: use TanStack Virtual's `Virtualizer` — it handles measurement, overscan, scroll alignment, and dynamic sizing.
|
|
230
|
+
Source: `docs/guide/virtualization.md`.
|
|
231
|
+
|
|
232
|
+
### HIGH Using the wrong row source
|
|
233
|
+
|
|
234
|
+
Wrong:
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
const virtualizer = useVirtualizer({ count: data.length /* … */ }) // bypasses sort/filter/paginate
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Correct:
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
const { rows } = table.getRowModel()
|
|
244
|
+
const virtualizer = useVirtualizer({ count: rows.length /* … */ })
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Always feed `table.getRowModel().rows.length` — that's the post-feature row array.
|
|
248
|
+
|
|
249
|
+
### HIGH Forgetting `position: relative` on the scroll parent / `position: absolute` on rows
|
|
250
|
+
|
|
251
|
+
Wrong:
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
<div ref={scrollRef} style={{ overflow: 'auto', height: 800 }}>
|
|
255
|
+
<tbody style={{ height: rowVirtualizer.getTotalSize() }}>{/* rows */}</tbody>
|
|
256
|
+
</div>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Correct: the absolute rows need a `position: relative` ancestor with the total height set. Without it, rows stack at the top.
|
|
260
|
+
|
|
261
|
+
### HIGH Forgetting to set up `measureElement` for dynamic sizing
|
|
262
|
+
|
|
263
|
+
Wrong: rows render but `estimateSize` is wrong and rows overlap or leave gaps.
|
|
264
|
+
|
|
265
|
+
Correct: attach `ref={(node) => rowVirtualizer.measureElement(node!)}` to each row element so the virtualizer can measure actual size.
|
|
266
|
+
|
|
267
|
+
### MEDIUM Mixing virtualization with `manualPagination`
|
|
268
|
+
|
|
269
|
+
You usually don't need both — server pagination already limits the row count. Virtualize when the client holds the full dataset.
|
|
270
|
+
|
|
271
|
+
## See Also
|
|
272
|
+
|
|
273
|
+
- `tanstack-table/preact/table-state` — Subscribe for fine-grained re-renders.
|
|
274
|
+
- `tanstack-table/preact/production-readiness` — narrow selectors, stable refs.
|
|
275
|
+
- `tanstack-table/row-expanding` — virtualizing rows with sub-component rows requires variable height + measureElement.
|