@tanstack/svelte-table 9.0.0-alpha.47 → 9.0.0-alpha.48
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 +6 -4
- package/skills/svelte/client-to-server/SKILL.md +238 -0
- package/skills/svelte/compose-with-tanstack-form/SKILL.md +295 -0
- package/skills/svelte/compose-with-tanstack-pacer/SKILL.md +176 -0
- package/skills/svelte/compose-with-tanstack-query/SKILL.md +299 -0
- package/skills/svelte/compose-with-tanstack-store/SKILL.md +277 -0
- package/skills/svelte/compose-with-tanstack-virtual/SKILL.md +286 -0
- package/skills/svelte/getting-started/SKILL.md +340 -0
- package/skills/svelte/migrate-v8-to-v9/SKILL.md +256 -0
- package/skills/svelte/production-readiness/SKILL.md +256 -0
- package/skills/svelte/table-state/SKILL.md +441 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: svelte/compose-with-tanstack-store
|
|
3
|
+
description: >
|
|
4
|
+
TanStack Table v9 is built on TanStack Store. Each state slice (sorting, pagination,
|
|
5
|
+
rowSelection, columnFilters, ...) is a separate atom. In Svelte, `@tanstack/svelte-store`
|
|
6
|
+
exposes `createAtom`, `useSelector`, `shallow`. Read `table.atoms.<slice>` per slice,
|
|
7
|
+
`table.store` flat, or `table.state` for the selector projection. Subscribe with
|
|
8
|
+
`subscribeTable(atom, selector?)` (returns `.current`). Own a slice externally with
|
|
9
|
+
`createAtom` + `atoms: { sorting: sortingAtom }`. Svelte 5+ only — `$state` / `$derived.by` /
|
|
10
|
+
`$effect.pre` reactivity.
|
|
11
|
+
type: composition
|
|
12
|
+
library: tanstack-table
|
|
13
|
+
framework: svelte
|
|
14
|
+
library_version: '9.0.0-alpha.47'
|
|
15
|
+
requires:
|
|
16
|
+
- state-management
|
|
17
|
+
sources:
|
|
18
|
+
- TanStack/table:docs/framework/svelte/guide/table-state.md
|
|
19
|
+
- TanStack/table:packages/svelte-table/src/reactivity.svelte.ts
|
|
20
|
+
- TanStack/table:packages/svelte-table/src/subscribe.ts
|
|
21
|
+
- TanStack/table:examples/svelte/basic-external-atoms/
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# Compose with TanStack Store (Svelte)
|
|
25
|
+
|
|
26
|
+
`@tanstack/svelte-store` is the reactive primitive under `@tanstack/svelte-table` v9. The
|
|
27
|
+
table doesn't merely _use_ Store — its entire reactivity model is built from Store atoms with
|
|
28
|
+
rune backings.
|
|
29
|
+
|
|
30
|
+
## Mental model — three read surfaces
|
|
31
|
+
|
|
32
|
+
A registered v9 table exposes:
|
|
33
|
+
|
|
34
|
+
| Surface | Shape | When to use |
|
|
35
|
+
| --------------------- | --------------------------- | ------------------------------------------ |
|
|
36
|
+
| `table.atoms.<slice>` | `ReadonlyAtom<TSlice>` | Per-slice subscription / `.get()` snapshot |
|
|
37
|
+
| `table.store` | `ReadonlyStore<FlatState>` | Flat snapshot across registered slices |
|
|
38
|
+
| `table.state` | `TSelected` (from selector) | The selector projection (Svelte-only) |
|
|
39
|
+
|
|
40
|
+
Plus the writable internals:
|
|
41
|
+
|
|
42
|
+
- `table.baseAtoms.<slice>` — writable atom for state the table owns.
|
|
43
|
+
|
|
44
|
+
If a slice is supplied externally via `atoms`, `table.atoms.<slice>` reads from your atom and
|
|
45
|
+
`table.baseAtoms.<slice>` is unused for that slice.
|
|
46
|
+
|
|
47
|
+
## The Svelte bindings (what `svelteReactivity()` actually does)
|
|
48
|
+
|
|
49
|
+
The Svelte adapter ships `svelteReactivity()` and installs it as `coreReativityFeature`. It
|
|
50
|
+
maps Store primitives to runes:
|
|
51
|
+
|
|
52
|
+
- Readonly atoms → `$derived.by(fn)`
|
|
53
|
+
- Writable atoms → `$state(initialValue)`
|
|
54
|
+
- Subscriptions → `$effect.root` + `$effect`
|
|
55
|
+
- Batch → `flushSync`
|
|
56
|
+
|
|
57
|
+
This is why simple atom reads inside `.svelte` components (templates, `$derived`, `$effect`)
|
|
58
|
+
participate in reactivity automatically. There is no React-style `useStore` requirement.
|
|
59
|
+
|
|
60
|
+
## Pattern 1 — Read a slice without subscribing
|
|
61
|
+
|
|
62
|
+
For event handlers, async work, exports, anything outside of reactive markup. Cheap, no
|
|
63
|
+
subscription setup.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import type { SortingState } from '@tanstack/svelte-table'
|
|
67
|
+
|
|
68
|
+
function logSort() {
|
|
69
|
+
const sorting: SortingState = table.atoms.sorting.get()
|
|
70
|
+
console.log(sorting)
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`table.store.state` is the full snapshot equivalent.
|
|
75
|
+
|
|
76
|
+
## Pattern 2 — Reactive selector via `createTable`
|
|
77
|
+
|
|
78
|
+
The second argument to `createTable` is a TanStack Store selector. The result is exposed on
|
|
79
|
+
`table.state`. The default selector is `(state) => state`.
|
|
80
|
+
|
|
81
|
+
```svelte
|
|
82
|
+
<script lang="ts">
|
|
83
|
+
const table = createTable(
|
|
84
|
+
{
|
|
85
|
+
_features,
|
|
86
|
+
_rowModels: { paginatedRowModel: createPaginatedRowModel() },
|
|
87
|
+
columns,
|
|
88
|
+
get data() {
|
|
89
|
+
return data
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
(state) => ({
|
|
93
|
+
pageIndex: state.pagination.pageIndex,
|
|
94
|
+
pageSize: state.pagination.pageSize,
|
|
95
|
+
}),
|
|
96
|
+
)
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<strong>Page {table.state.pageIndex + 1}</strong>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The narrower the selector, the less your markup re-renders.
|
|
103
|
+
|
|
104
|
+
## Pattern 3 — Per-block subscription with `subscribeTable`
|
|
105
|
+
|
|
106
|
+
`subscribeTable(source, selector?)` is the dedicated per-component subscription. It uses
|
|
107
|
+
`shallow` compare and exposes a `.current` accessor.
|
|
108
|
+
|
|
109
|
+
```svelte
|
|
110
|
+
<script lang="ts">
|
|
111
|
+
import { subscribeTable } from '@tanstack/svelte-table'
|
|
112
|
+
|
|
113
|
+
// whole slice
|
|
114
|
+
const pagination = subscribeTable(table.atoms.pagination)
|
|
115
|
+
|
|
116
|
+
// narrowed
|
|
117
|
+
const pageSize = subscribeTable(table.atoms.pagination, (p) => p.pageSize)
|
|
118
|
+
|
|
119
|
+
// works on table.store too
|
|
120
|
+
const fullSnapshot = subscribeTable(table.store)
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<span
|
|
124
|
+
>Page {pagination.current.pageIndex + 1} ({pageSize.current} per page)</span
|
|
125
|
+
>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Inside per-row components, `subscribeTable(table.atoms.rowSelection, (s) => !!s[row.id])` keeps
|
|
129
|
+
that row's checkbox reactive without subscribing to the entire selection map.
|
|
130
|
+
|
|
131
|
+
## Pattern 4 — Own a slice externally with `createAtom`
|
|
132
|
+
|
|
133
|
+
When the app should own a slice — share across components, sync with URL, persist to storage —
|
|
134
|
+
create a stable atom and hand it to the table via `atoms`.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { createAtom, useSelector } from '@tanstack/svelte-store'
|
|
138
|
+
import {
|
|
139
|
+
createTable,
|
|
140
|
+
rowPaginationFeature,
|
|
141
|
+
rowSortingFeature,
|
|
142
|
+
tableFeatures,
|
|
143
|
+
type PaginationState,
|
|
144
|
+
type SortingState,
|
|
145
|
+
} from '@tanstack/svelte-table'
|
|
146
|
+
|
|
147
|
+
const _features = tableFeatures({
|
|
148
|
+
rowPaginationFeature,
|
|
149
|
+
rowSortingFeature,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const sortingAtom = createAtom<SortingState>([])
|
|
153
|
+
const paginationAtom = createAtom<PaginationState>({
|
|
154
|
+
pageIndex: 0,
|
|
155
|
+
pageSize: 10,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Optional: a Svelte-reactive view onto each atom for use in markup.
|
|
159
|
+
const sorting = useSelector(sortingAtom)
|
|
160
|
+
const pagination = useSelector(paginationAtom)
|
|
161
|
+
|
|
162
|
+
const table = createTable({
|
|
163
|
+
_features,
|
|
164
|
+
_rowModels: {
|
|
165
|
+
/* ... */
|
|
166
|
+
},
|
|
167
|
+
columns,
|
|
168
|
+
get data() {
|
|
169
|
+
return data
|
|
170
|
+
},
|
|
171
|
+
atoms: {
|
|
172
|
+
sorting: sortingAtom,
|
|
173
|
+
pagination: paginationAtom,
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// table.setPageIndex(2) writes through paginationAtom.
|
|
178
|
+
// paginationAtom.set(...) updates table.atoms.pagination immediately.
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Atom precedence: external `atoms.<slice>` wins over external `state.<slice>` which writes
|
|
182
|
+
into the internal `baseAtoms.<slice>`. **Never combine them on the same slice.**
|
|
183
|
+
|
|
184
|
+
## Pattern 5 — Cross-component / cross-module state
|
|
185
|
+
|
|
186
|
+
Because atoms are first-class subscribable values, you can read them outside the table component.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
// stores/table-state.ts
|
|
190
|
+
import { createAtom } from '@tanstack/svelte-store'
|
|
191
|
+
import type { RowSelectionState } from '@tanstack/svelte-table'
|
|
192
|
+
|
|
193
|
+
export const rowSelectionAtom = createAtom<RowSelectionState>({})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```svelte
|
|
197
|
+
<!-- Toolbar.svelte -->
|
|
198
|
+
<script lang="ts">
|
|
199
|
+
import { useSelector } from '@tanstack/svelte-store'
|
|
200
|
+
import { rowSelectionAtom } from './stores/table-state'
|
|
201
|
+
|
|
202
|
+
const selection = useSelector(rowSelectionAtom)
|
|
203
|
+
const selectedCount = $derived(
|
|
204
|
+
Object.values(selection.current).filter(Boolean).length,
|
|
205
|
+
)
|
|
206
|
+
</script>
|
|
207
|
+
|
|
208
|
+
<button disabled={selectedCount === 0}>Delete {selectedCount}</button>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```svelte
|
|
212
|
+
<!-- TablePage.svelte -->
|
|
213
|
+
<script lang="ts">
|
|
214
|
+
import { rowSelectionAtom } from './stores/table-state'
|
|
215
|
+
|
|
216
|
+
const table = createTable({
|
|
217
|
+
_features,
|
|
218
|
+
_rowModels: {
|
|
219
|
+
/* ... */
|
|
220
|
+
},
|
|
221
|
+
columns,
|
|
222
|
+
get data() {
|
|
223
|
+
return data
|
|
224
|
+
},
|
|
225
|
+
atoms: { rowSelection: rowSelectionAtom },
|
|
226
|
+
})
|
|
227
|
+
</script>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Pattern 6 — `useSelector` with custom equality
|
|
231
|
+
|
|
232
|
+
`useSelector(source, selector, { compare })` lets you switch comparison strategies — useful
|
|
233
|
+
for object selectors so you don't re-fire on every reference change.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import { shallow, useSelector } from '@tanstack/svelte-store'
|
|
237
|
+
|
|
238
|
+
const filterValues = useSelector(
|
|
239
|
+
table.atoms.columnFilters,
|
|
240
|
+
(filters) => Object.fromEntries(filters.map((f) => [f.id, f.value])),
|
|
241
|
+
{ compare: shallow },
|
|
242
|
+
)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`subscribeTable` already uses `shallow` by default, so prefer it for table sources unless you
|
|
246
|
+
need a custom compare.
|
|
247
|
+
|
|
248
|
+
## Pattern 7 — Direct base-atom writes (last resort)
|
|
249
|
+
|
|
250
|
+
When a slice is internally owned and you really need to write outside a feature API:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
table.baseAtoms.pagination.set((old) => ({ ...old, pageIndex: 0 }))
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Do not do this for externally-owned slices — write to your external atom instead. The base
|
|
257
|
+
atom is dormant in that case and your write will be silently ignored next sync.
|
|
258
|
+
|
|
259
|
+
## Common failure modes
|
|
260
|
+
|
|
261
|
+
- **Reading a slice that wasn't registered.** `table.atoms.rowSelection` is `undefined` if
|
|
262
|
+
`rowSelectionFeature` isn't in `_features`. TS will catch it if you used `tableFeatures()`.
|
|
263
|
+
- **Creating atoms inside reactive blocks.** Atoms must be stable. Module scope or top-level
|
|
264
|
+
component scope, never inside `$derived` / `$effect`.
|
|
265
|
+
- **`useSelector` without `.current`.** `selection.pageIndex` is wrong — `selection.current.pageIndex`.
|
|
266
|
+
- **Mixing `atoms.X` and `state.X`.** Atom wins, callback never fires.
|
|
267
|
+
- **`tableState` as a plain object.** No reactivity. Use `subscribeTable`, `useSelector`, or
|
|
268
|
+
the `createTable` selector.
|
|
269
|
+
- **Reimplementing `useSelector` with `$effect`.** Built-in is more efficient and uses
|
|
270
|
+
shallow compare.
|
|
271
|
+
|
|
272
|
+
## Related skills
|
|
273
|
+
|
|
274
|
+
- `tanstack-table/svelte/table-state` — full reactivity model and selector patterns.
|
|
275
|
+
- `tanstack-table/core/state-management` — atom precedence rules.
|
|
276
|
+
- `tanstack-table/svelte/client-to-server` — atoms as the data-driver for server queries.
|
|
277
|
+
- `tanstack-table/svelte/production-readiness` — selector / subscription tuning.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: svelte/compose-with-tanstack-virtual
|
|
3
|
+
description: >
|
|
4
|
+
`@tanstack/svelte-table` does not include virtualization — pair it with
|
|
5
|
+
`@tanstack/svelte-virtual`. Use `createVirtualizer({ count, estimateSize, getScrollElement,
|
|
6
|
+
... })`, feed `table.getRowModel().rows.length` as `count`, render only
|
|
7
|
+
`$rowVirtualizer.getVirtualItems()`, position rows with `transform: translateY(...)` and a
|
|
8
|
+
container of `getTotalSize()`. Use `measureElement` actions for dynamic row heights. Svelte 5+
|
|
9
|
+
only — `$state` for refs, `$effect` to sync count.
|
|
10
|
+
type: composition
|
|
11
|
+
library: tanstack-table
|
|
12
|
+
framework: svelte
|
|
13
|
+
library_version: '9.0.0-alpha.47'
|
|
14
|
+
requires:
|
|
15
|
+
- svelte/table-state
|
|
16
|
+
- row-expanding
|
|
17
|
+
sources:
|
|
18
|
+
- TanStack/table:docs/guide/virtualization.md
|
|
19
|
+
- TanStack/table:examples/svelte/virtualized-rows/
|
|
20
|
+
- TanStack/table:examples/svelte/virtualized-columns/
|
|
21
|
+
- TanStack/table:examples/svelte/virtualized-infinite-scrolling/
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# Compose with TanStack Virtual (Svelte)
|
|
25
|
+
|
|
26
|
+
TanStack Table is **not** a virtualizer. For lists / grids past a few thousand rows (or with
|
|
27
|
+
heavy per-row markup), pair it with `@tanstack/svelte-virtual`.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm add @tanstack/svelte-virtual
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Core mental model
|
|
36
|
+
|
|
37
|
+
- TanStack Table gives you `rows: row[]` (already filtered / sorted / paged / grouped).
|
|
38
|
+
- TanStack Virtual takes `count` (the length) and returns `virtualItems` (the slice currently
|
|
39
|
+
in view).
|
|
40
|
+
- You render only those virtual items, absolutely positioned, inside a container sized to
|
|
41
|
+
`getTotalSize()` pixels.
|
|
42
|
+
|
|
43
|
+
`createVirtualizer` returns a Svelte store. Read its current value with `$rowVirtualizer` or
|
|
44
|
+
`get(rowVirtualizer)` (from `svelte/store`).
|
|
45
|
+
|
|
46
|
+
## Basic row virtualization
|
|
47
|
+
|
|
48
|
+
```svelte
|
|
49
|
+
<script lang="ts">
|
|
50
|
+
import {
|
|
51
|
+
columnSizingFeature,
|
|
52
|
+
createSortedRowModel,
|
|
53
|
+
createTable,
|
|
54
|
+
rowSortingFeature,
|
|
55
|
+
sortFns,
|
|
56
|
+
tableFeatures,
|
|
57
|
+
FlexRender,
|
|
58
|
+
} from '@tanstack/svelte-table'
|
|
59
|
+
import { createVirtualizer } from '@tanstack/svelte-virtual'
|
|
60
|
+
import { get } from 'svelte/store'
|
|
61
|
+
|
|
62
|
+
const _features = tableFeatures({ columnSizingFeature, rowSortingFeature })
|
|
63
|
+
|
|
64
|
+
let data = $state<Person[]>(makeData(200_000))
|
|
65
|
+
|
|
66
|
+
const table = createTable({
|
|
67
|
+
_features,
|
|
68
|
+
_rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
|
|
69
|
+
columns,
|
|
70
|
+
get data() {
|
|
71
|
+
return data
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
let tableContainerRef = $state<HTMLDivElement | undefined>(undefined)
|
|
76
|
+
|
|
77
|
+
const rows = $derived(table.getRowModel().rows)
|
|
78
|
+
|
|
79
|
+
const rowVirtualizer = createVirtualizer({
|
|
80
|
+
get count() {
|
|
81
|
+
return rows.length
|
|
82
|
+
},
|
|
83
|
+
estimateSize: () => 33,
|
|
84
|
+
getScrollElement: () => tableContainerRef ?? null,
|
|
85
|
+
overscan: 5,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// svelte-virtual's store adapter does not reactively track getter options;
|
|
89
|
+
// push updates explicitly when ref / count change.
|
|
90
|
+
$effect(() => {
|
|
91
|
+
if (tableContainerRef) {
|
|
92
|
+
get(rowVirtualizer).setOptions({
|
|
93
|
+
getScrollElement: () => tableContainerRef ?? null,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
$effect(() => {
|
|
99
|
+
get(rowVirtualizer).setOptions({ count: rows.length })
|
|
100
|
+
})
|
|
101
|
+
</script>
|
|
102
|
+
|
|
103
|
+
<div
|
|
104
|
+
bind:this={tableContainerRef}
|
|
105
|
+
style="overflow: auto; position: relative; height: 800px;"
|
|
106
|
+
>
|
|
107
|
+
<table style="display: grid;">
|
|
108
|
+
<thead style="display: grid; position: sticky; top: 0; z-index: 1;">
|
|
109
|
+
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
|
110
|
+
<tr style="display: flex; width: 100%;">
|
|
111
|
+
{#each headerGroup.headers as header (header.id)}
|
|
112
|
+
<th style="display: flex; width: {header.getSize()}px;">
|
|
113
|
+
<FlexRender {header} />
|
|
114
|
+
</th>
|
|
115
|
+
{/each}
|
|
116
|
+
</tr>
|
|
117
|
+
{/each}
|
|
118
|
+
</thead>
|
|
119
|
+
<tbody
|
|
120
|
+
style="display: grid; position: relative; height: {$rowVirtualizer.getTotalSize()}px;"
|
|
121
|
+
>
|
|
122
|
+
{#each $rowVirtualizer.getVirtualItems() as virtualRow (virtualRow.index)}
|
|
123
|
+
{@const row = rows[virtualRow.index]}
|
|
124
|
+
<tr
|
|
125
|
+
data-index={virtualRow.index}
|
|
126
|
+
style="display: flex; position: absolute; transform: translateY({virtualRow.start}px); width: 100%;"
|
|
127
|
+
>
|
|
128
|
+
{#each row.getAllCells() as cell (cell.id)}
|
|
129
|
+
<td style="display: flex; width: {cell.column.getSize()}px;">
|
|
130
|
+
<FlexRender {cell} />
|
|
131
|
+
</td>
|
|
132
|
+
{/each}
|
|
133
|
+
</tr>
|
|
134
|
+
{/each}
|
|
135
|
+
</tbody>
|
|
136
|
+
</table>
|
|
137
|
+
</div>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Why `display: grid` / `flex` instead of native table layout? Because the rows are absolutely
|
|
141
|
+
positioned, the browser's native table layout algorithm can't size columns from non-flowing
|
|
142
|
+
rows. CSS layout takes over.
|
|
143
|
+
|
|
144
|
+
## Dynamic row heights (`measureElement`)
|
|
145
|
+
|
|
146
|
+
For variable row heights (multi-line cells, expanding rows), measure rendered nodes with the
|
|
147
|
+
virtualizer's `measureElement` API.
|
|
148
|
+
|
|
149
|
+
```svelte
|
|
150
|
+
<script lang="ts">
|
|
151
|
+
const rowVirtualizer = createVirtualizer({
|
|
152
|
+
get count() {
|
|
153
|
+
return rows.length
|
|
154
|
+
},
|
|
155
|
+
estimateSize: () => 33,
|
|
156
|
+
getScrollElement: () => tableContainerRef ?? null,
|
|
157
|
+
measureElement:
|
|
158
|
+
typeof window !== 'undefined' &&
|
|
159
|
+
navigator.userAgent.indexOf('Firefox') === -1
|
|
160
|
+
? (element) => element.getBoundingClientRect().height
|
|
161
|
+
: undefined,
|
|
162
|
+
overscan: 5,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Svelte action wrapping the virtualizer's measure call.
|
|
166
|
+
function measureElement(node: HTMLTableRowElement) {
|
|
167
|
+
get(rowVirtualizer).measureElement(node)
|
|
168
|
+
}
|
|
169
|
+
</script>
|
|
170
|
+
|
|
171
|
+
<tr use:measureElement data-index={virtualRow.index} ...>...</tr>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`data-index` is required — the virtualizer uses it to map a measured element back to its
|
|
175
|
+
virtual item.
|
|
176
|
+
|
|
177
|
+
> Firefox measures table-border rows incorrectly. The above guards against measuring there and
|
|
178
|
+
> falls back to the estimate.
|
|
179
|
+
|
|
180
|
+
## Column virtualization
|
|
181
|
+
|
|
182
|
+
`createVirtualizer` with `horizontal: true` against `table.getVisibleLeafColumns()`. Same
|
|
183
|
+
pattern — only render `getVirtualItems()` cells per row, position with `translateX`.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const columnVirtualizer = createVirtualizer({
|
|
187
|
+
get count() {
|
|
188
|
+
return visibleColumns.length
|
|
189
|
+
},
|
|
190
|
+
estimateSize: (index) => visibleColumns[index].getSize(),
|
|
191
|
+
getScrollElement: () => tableContainerRef ?? null,
|
|
192
|
+
horizontal: true,
|
|
193
|
+
overscan: 3,
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
For combined row + column virtualization, render the row virtualizer's items, and inside each
|
|
198
|
+
row render the column virtualizer's items. See `examples/svelte/virtualized-columns/`.
|
|
199
|
+
|
|
200
|
+
## Infinite scroll (load more on near-bottom)
|
|
201
|
+
|
|
202
|
+
Subscribe to the virtualizer's `getVirtualItems()` and check the last one's index against your
|
|
203
|
+
total available count.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { createInfiniteQuery } from '@tanstack/svelte-query'
|
|
207
|
+
|
|
208
|
+
const infiniteQuery = createInfiniteQuery(() => ({
|
|
209
|
+
queryKey: ['people-infinite'],
|
|
210
|
+
queryFn: ({ pageParam }) => fetchPeople({ cursor: pageParam, pageSize: 50 }),
|
|
211
|
+
initialPageParam: undefined as string | undefined,
|
|
212
|
+
getNextPageParam: (last) => last.nextCursor,
|
|
213
|
+
}))
|
|
214
|
+
|
|
215
|
+
const flatData = $derived(
|
|
216
|
+
infiniteQuery.data?.pages.flatMap((p) => p.rows) ?? [],
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
const table = createTable({
|
|
220
|
+
_features: tableFeatures({}),
|
|
221
|
+
_rowModels: {},
|
|
222
|
+
columns,
|
|
223
|
+
get data() {
|
|
224
|
+
return flatData
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const rows = $derived(table.getRowModel().rows)
|
|
229
|
+
|
|
230
|
+
const rowVirtualizer = createVirtualizer({
|
|
231
|
+
get count() {
|
|
232
|
+
return rows.length
|
|
233
|
+
},
|
|
234
|
+
estimateSize: () => 33,
|
|
235
|
+
getScrollElement: () => tableContainerRef ?? null,
|
|
236
|
+
overscan: 10,
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
$effect(() => {
|
|
240
|
+
const items = $rowVirtualizer.getVirtualItems()
|
|
241
|
+
const last = items[items.length - 1]
|
|
242
|
+
if (
|
|
243
|
+
last &&
|
|
244
|
+
last.index >= rows.length - 1 &&
|
|
245
|
+
infiniteQuery.hasNextPage &&
|
|
246
|
+
!infiniteQuery.isFetchingNextPage
|
|
247
|
+
) {
|
|
248
|
+
infiniteQuery.fetchNextPage()
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Interaction with row expanding
|
|
254
|
+
|
|
255
|
+
If `rowExpandingFeature` is registered, `table.getRowModel().rows` already flattens expanded
|
|
256
|
+
sub-rows into a single sequential list. The virtualizer just sees a longer list — no special
|
|
257
|
+
handling needed.
|
|
258
|
+
|
|
259
|
+
For variable row heights driven by expand state, you'll want `measureElement` so the
|
|
260
|
+
container resizes when a row expands.
|
|
261
|
+
|
|
262
|
+
## Pagination vs. virtualization
|
|
263
|
+
|
|
264
|
+
Pick one. Virtualization is for "render all rows but render only the visible window".
|
|
265
|
+
Pagination is for "the user navigates discrete pages". Combining them usually means you don't
|
|
266
|
+
need either — drop pagination and let the virtualizer handle the rendering window.
|
|
267
|
+
|
|
268
|
+
## Common failure modes
|
|
269
|
+
|
|
270
|
+
- **Forgot to push `count` updates.** `svelte-virtual` does not auto-track `get count()` —
|
|
271
|
+
use `$effect` + `setOptions({ count })`.
|
|
272
|
+
- **Native table layout with virtualized rows.** Columns collapse because absolutely
|
|
273
|
+
positioned rows don't contribute to layout. Use `display: grid` / `flex`.
|
|
274
|
+
- **No `data-index` on `<tr>`.** `measureElement` can't map back to virtual items.
|
|
275
|
+
- **No `transform: translateY`.** Rows render at `top: 0` and stack visually.
|
|
276
|
+
- **Missing container `height`.** No overflow, no scroll, no virtualization.
|
|
277
|
+
- **Calling `get(rowVirtualizer).getVirtualItems()` in template.** Wrong access pattern;
|
|
278
|
+
use `$rowVirtualizer.getVirtualItems()` (store auto-subscribe) or be sure to
|
|
279
|
+
`import { get } from 'svelte/store'`.
|
|
280
|
+
- **Reimplementing windowing manually.** Don't.
|
|
281
|
+
|
|
282
|
+
## Related skills
|
|
283
|
+
|
|
284
|
+
- `tanstack-table/svelte/table-state` — `getRowModel()` and the reactivity model.
|
|
285
|
+
- `tanstack-table/core/row-expanding` — flattening sub-rows for virtualization.
|
|
286
|
+
- `tanstack-table/svelte/compose-with-tanstack-query` — infinite-scroll data source.
|