@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.
@@ -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.