@tanstack/vue-table 9.0.0-beta.40 → 9.0.0-beta.43

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.
@@ -1,344 +0,0 @@
1
- ---
2
- name: vue/compose-with-tanstack-virtual
3
- description: >
4
- `@tanstack/vue-table` v9 does not include virtualization — pair with `@tanstack/vue-virtual`.
5
- Standard row-virtualization pattern: get the row array from `table.getRowModel().rows`, feed
6
- `rows.length` to `useVirtualizer(computed(() => ({ count, estimateSize, getScrollElement,
7
- overscan })))`, iterate `rowVirtualizer.value.getVirtualItems()` instead of `rows.map`,
8
- absolute-position each row with `transform: translateY(virtualRow.start)px`, and use
9
- `display: grid` on `<table>`/`<thead>`/`<tbody>`. The virtualizer MUST live in the deepest
10
- component possible so unrelated state changes don't re-run it. Column virtualization mirrors
11
- the shape with `horizontal: true` plus padding-left/right placeholder cells. Skip
12
- `measureElement` on Firefox — it returns inconsistent table-row heights.
13
- type: composition
14
- library: tanstack-table
15
- framework: vue
16
- library_version: '9.0.0-alpha.48'
17
- requires:
18
- - vue/table-state
19
- - row-expanding
20
- sources:
21
- - docs/guide/virtualization.md
22
- - examples/vue/virtualized-rows/src/App.vue
23
- - examples/vue/virtualized-columns/
24
- - examples/vue/virtualized-infinite-scrolling/
25
- ---
26
-
27
- # Compose @tanstack/vue-table with @tanstack/vue-virtual
28
-
29
- ## Dependencies
30
-
31
- ```bash
32
- pnpm add @tanstack/vue-table @tanstack/vue-virtual
33
- ```
34
-
35
- `@tanstack/vue-virtual` is the Vue adapter for TanStack Virtual. Its key Vue-isms:
36
-
37
- - `useVirtualizer(optionsRef)` accepts a `ref`/`computed` of options (or a getter), and
38
- returns a `Ref<Virtualizer>` — you read `.value.getVirtualItems()`, `.value.getTotalSize()`, etc.
39
- - `getScrollElement` resolves the scrollable container via a template ref.
40
-
41
- ## Setup — row virtualization, the standard pattern
42
-
43
- ```vue
44
- <script setup lang="ts">
45
- import { computed, ref } from 'vue'
46
- import {
47
- FlexRender,
48
- columnSizingFeature,
49
- createSortedRowModel,
50
- rowSortingFeature,
51
- sortFns,
52
- tableFeatures,
53
- useTable,
54
- type ColumnDef,
55
- } from '@tanstack/vue-table'
56
- import { useVirtualizer } from '@tanstack/vue-virtual'
57
- import { makeData, type Person } from './makeData'
58
-
59
- const features = tableFeatures({
60
- columnSizingFeature,
61
- rowSortingFeature,
62
- sortedRowModel: createSortedRowModel(),
63
- sortFns,
64
- })
65
-
66
- const columns: ColumnDef<typeof features, Person>[] = [
67
- { accessorKey: 'firstName' },
68
- { accessorKey: 'lastName' },
69
- { accessorKey: 'age' },
70
- ]
71
-
72
- const data = ref<Person[]>(makeData(50_000))
73
-
74
- const table = useTable({
75
- features,
76
- columns,
77
- data,
78
- })
79
-
80
- const rows = computed(() => table.getRowModel().rows)
81
-
82
- // 1) Template ref to the scrollable container.
83
- const tableContainerRef = ref<HTMLDivElement | null>(null)
84
-
85
- // 2) Reactive virtualizer options — recomputed when rows.length changes.
86
- const rowVirtualizerOptions = computed(() => ({
87
- count: rows.value.length,
88
- estimateSize: () => 33,
89
- getScrollElement: () => tableContainerRef.value,
90
- overscan: 5,
91
- }))
92
-
93
- const rowVirtualizer = useVirtualizer(rowVirtualizerOptions)
94
-
95
- const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems())
96
- const totalSize = computed(() => rowVirtualizer.value.getTotalSize())
97
- </script>
98
-
99
- <template>
100
- <div
101
- ref="tableContainerRef"
102
- :style="{ overflow: 'auto', position: 'relative', height: '800px' }"
103
- >
104
- <!-- display: grid is required for absolute-positioned virtual rows -->
105
- <table :style="{ display: 'grid' }">
106
- <thead
107
- :style="{ display: 'grid', position: 'sticky', top: 0, zIndex: 1 }"
108
- >
109
- <tr
110
- v-for="hg in table.getHeaderGroups()"
111
- :key="hg.id"
112
- :style="{ display: 'flex', width: '100%' }"
113
- >
114
- <th
115
- v-for="h in hg.headers"
116
- :key="h.id"
117
- :style="{ width: `${h.getSize()}px` }"
118
- >
119
- <FlexRender v-if="!h.isPlaceholder" :header="h" />
120
- </th>
121
- </tr>
122
- </thead>
123
- <tbody
124
- :style="{
125
- display: 'grid',
126
- height: `${totalSize}px`,
127
- position: 'relative',
128
- }"
129
- >
130
- <tr
131
- v-for="vRow in virtualRows"
132
- :key="rows[vRow.index].id"
133
- :data-index="vRow.index"
134
- :style="{
135
- display: 'flex',
136
- position: 'absolute',
137
- transform: `translateY(${vRow.start}px)`,
138
- width: '100%',
139
- }"
140
- >
141
- <td
142
- v-for="cell in rows[vRow.index].getAllCells()"
143
- :key="cell.id"
144
- :style="{ display: 'flex', width: `${cell.column.getSize()}px` }"
145
- >
146
- <FlexRender :cell="cell" />
147
- </td>
148
- </tr>
149
- </tbody>
150
- </table>
151
- </div>
152
- </template>
153
- ```
154
-
155
- Source: `examples/vue/virtualized-rows/src/App.vue`.
156
-
157
- ## Core Patterns
158
-
159
- ### 1. Keep `useVirtualizer` in the deepest possible component
160
-
161
- ```vue
162
- <!-- ✅ Body owns the virtualizer. Filter input changes in App.vue don't re-run it. -->
163
- <!-- App.vue -->
164
- <TableContainer ref="tableContainerRef">
165
- <TableBody :table="table" :container-ref="tableContainerRef" />
166
- </TableContainer>
167
-
168
- <!-- TableBody.vue: useVirtualizer lives here. -->
169
- ```
170
-
171
- This is the single most important perf rule. Putting `useVirtualizer` in the same component
172
- as `useTable` means any state change in that component re-runs the virtualizer, blowing
173
- scroll position and measurement cache.
174
-
175
- ### 2. `display: grid` + absolute-positioned rows
176
-
177
- Semantic `<table>` markup still works, but the _layout_ must be CSS grid + flexbox. Without
178
- `display: grid` on `<table>`/`<thead>`/`<tbody>` plus `position: absolute` + `transform:
179
- translateY(start)px` on each row, virtual rows stack or overlap.
180
-
181
- ### 3. Column virtualization
182
-
183
- ```ts
184
- const columnVirtualizer = useVirtualizer(
185
- computed(() => ({
186
- count: columns.length,
187
- estimateSize: (index) => columns[index].size ?? 150,
188
- getScrollElement: () => tableContainerRef.value,
189
- horizontal: true,
190
- overscan: 3,
191
- })),
192
- )
193
-
194
- const virtualCols = computed(() => columnVirtualizer.value.getVirtualItems())
195
-
196
- // Then in template, render only virtualCols. Pad with empty cells at left/right:
197
- const virtualPaddingLeft = computed(() => virtualCols.value[0]?.start ?? 0)
198
- const virtualPaddingRight = computed(() => {
199
- const last = virtualCols.value[virtualCols.value.length - 1]
200
- return columnVirtualizer.value.getTotalSize() - (last?.end ?? 0)
201
- })
202
- ```
203
-
204
- Without left/right padding placeholder cells, visible columns slide left as you scroll because
205
- unrendered columns aren't taking up scroll space.
206
-
207
- ### 4. Dynamic row heights via `measureElement`
208
-
209
- ```ts
210
- const rowVirtualizerOptions = computed(() => ({
211
- count: rows.value.length,
212
- estimateSize: () => 33,
213
- getScrollElement: () => tableContainerRef.value,
214
- // Firefox returns inconsistent table-row heights — skip there.
215
- measureElement:
216
- typeof window !== 'undefined' &&
217
- navigator.userAgent.indexOf('Firefox') === -1
218
- ? (el) => el.getBoundingClientRect().height
219
- : undefined,
220
- overscan: 5,
221
- }))
222
- ```
223
-
224
- In templates, wire `measureElement` via a ref callback:
225
-
226
- ```vue
227
- <tr :ref="(el) => rowVirtualizer.value.measureElement(el as Element)">
228
- ...
229
- </tr>
230
- ```
231
-
232
- ### 5. Infinite scroll
233
-
234
- Combine `useInfiniteQuery` from `@tanstack/vue-query` with a scroll-event handler that calls
235
- `fetchNextPage()` when within ~500px of the bottom. **Set `manualSorting: true`** so a new
236
- query fires on sort changes — otherwise the table re-sorts already-fetched pages locally and
237
- scrambles order.
238
-
239
- ```ts
240
- const flatData = computed(
241
- () => infiniteQuery.data.value?.pages.flatMap((p) => p.rows) ?? [],
242
- )
243
-
244
- const onScroll = (e: Event) => {
245
- const t = e.target as HTMLDivElement
246
- if (
247
- t.scrollHeight - t.scrollTop - t.clientHeight < 500 &&
248
- !infiniteQuery.isFetching.value
249
- ) {
250
- infiniteQuery.fetchNextPage()
251
- }
252
- }
253
- ```
254
-
255
- Source: `examples/vue/virtualized-infinite-scrolling/`.
256
-
257
- ## Common Mistakes
258
-
259
- ### Putting `useVirtualizer` in the same component as `useTable` (CRITICAL)
260
-
261
- Any unrelated state change in that component re-runs the virtualizer. Move it down to a
262
- `TableBody.vue` component that takes the table as a prop.
263
-
264
- ### Forgetting `display: grid` on `<table>`/`<thead>`/`<tbody>` (CRITICAL)
265
-
266
- The semantic table layout fights with absolute positioning. Virtual rows stack on top of each
267
- other or overlap.
268
-
269
- ### Missing `transform: translateY(virtualRow.start)px` (CRITICAL)
270
-
271
- All rows render at `top: 0` and only the last few are visible. The virtualizer reports the
272
- correct `start` for each item; you must apply it.
273
-
274
- ### Using `measureElement` on Firefox (HIGH)
275
-
276
- Firefox returns inconsistent border-height measurements for `<tr>` elements — rows jitter on
277
- every scroll. Guard with `navigator.userAgent.indexOf('Firefox') === -1`.
278
-
279
- ### Passing options as a plain object instead of a `computed` / getter (HIGH — Vue-specific)
280
-
281
- ```ts
282
- // ❌ Static options — virtualizer doesn't re-run when rows.length changes.
283
- const rowVirtualizer = useVirtualizer({
284
- count: rows.value.length,
285
- // ...
286
- })
287
-
288
- // ✅ Reactive options.
289
- const rowVirtualizer = useVirtualizer(
290
- computed(() => ({
291
- count: rows.value.length,
292
- estimateSize: () => 33,
293
- getScrollElement: () => tableContainerRef.value,
294
- })),
295
- )
296
- ```
297
-
298
- ### Forgetting padding cells in column virtualization (HIGH)
299
-
300
- Without `virtualPaddingLeft` / `virtualPaddingRight` cells, columns slide horizontally as you
301
- scroll because unrendered columns aren't taking up scroll space.
302
-
303
- ### Forgetting `manualSorting: true` on infinite scroll (HIGH)
304
-
305
- The table re-sorts already-fetched pages every time a new page arrives, scrambling order.
306
-
307
- ### Reading `table.state` above the virtualizer (HIGH)
308
-
309
- Any reactive read of `table.state` in a parent component re-renders the parent → re-renders
310
- the virtualizer-owning child → loses scroll. Use the narrowest read at the lowest level
311
- (see `tanstack-table/vue/production-readiness`).
312
-
313
- ### Hallucinating React Virtual hooks in Vue code (CRITICAL)
314
-
315
- ```ts
316
- // ❌
317
- import { useVirtualizer } from '@tanstack/react-virtual'
318
-
319
- // ✅
320
- import { useVirtualizer } from '@tanstack/vue-virtual'
321
- ```
322
-
323
- Same name, Vue-specific reactivity contract — `useVirtualizer` returns a `Ref<Virtualizer>`
324
- in Vue, not a plain object.
325
-
326
- ### "API missing" — `getRowModel` returns nothing (CRITICAL — v9-specific)
327
-
328
- If `table.getRowModel().rows` is empty when data is loaded, the row-model feature for whatever
329
- slice you need (filtering/sorting/grouping) isn't registered. Add the feature, its factory, and
330
- its fn registry to `tableFeatures({...})`.
331
-
332
- ### Reimplementing virtualization manually (CRITICAL — #1 AI tell)
333
-
334
- Slice `rows` with `Array.slice(start, end)` based on scroll position is the classic
335
- re-invention. Use `useVirtualizer` — it handles overscan, dynamic heights, scroll-to-index,
336
- all of which the hand-rolled version skips.
337
-
338
- ## See Also
339
-
340
- - `tanstack-table/vue/production-readiness` — keep the virtualizer in a leaf component
341
- - `tanstack-table/vue/table-state` — narrow reads to avoid parent re-renders
342
- - `tanstack-table/vue/compose-with-tanstack-query` — infinite-scroll pairs with `useInfiniteQuery`
343
- - `tanstack-table/table-core/row-expanding` — virtualized + expanding interactions
344
- - `tanstack-table/table-core/column-layout` — column sizing/pinning + virtualization