@tanstack/svelte-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.
@@ -0,0 +1,176 @@
1
+ ---
2
+ name: svelte/compose-with-tanstack-pacer
3
+ description: >
4
+ Use `@tanstack/svelte-pacer` to debounce / throttle high-frequency writes that drive a
5
+ `@tanstack/svelte-table` v9 instance — column filter inputs and column resize state are the
6
+ two hot paths. Import `createDebouncer` (or `createThrottler`) from
7
+ `@tanstack/svelte-pacer/debouncer`, wrap the call site that hits `column.setFilterValue`,
8
+ `table.setGlobalFilter`, or commits a resize, and call `.maybeExecute(value)` on each event.
9
+ Svelte 5+ only — pacer instances live at component-init scope.
10
+ type: composition
11
+ library: tanstack-table
12
+ framework: svelte
13
+ library_version: '9.0.0-alpha.48'
14
+ requires:
15
+ - filtering
16
+ - column-layout
17
+ sources:
18
+ - TanStack/table:examples/svelte/with-tanstack-form/
19
+ - TanStack/table:docs/framework/svelte/guide/table-state.md
20
+ ---
21
+
22
+ # Compose with TanStack Pacer (Svelte)
23
+
24
+ Two places in a v9 table take state writes at event-loop rate: **filter inputs** (one
25
+ keystroke = one `setFilterValue`) and **column resizing** (one pointermove = one
26
+ `columnSizing` write). Without rate-limiting they either flood the network (server-side
27
+ filtering) or burn CPU on tens of thousands of re-renders per drag.
28
+
29
+ `@tanstack/svelte-pacer` gives you `createDebouncer`, `createThrottler`, and friends. Wrap
30
+ the call site and you're done.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pnpm add @tanstack/svelte-pacer
36
+ ```
37
+
38
+ ## Debounce a column filter input
39
+
40
+ Client-side debouncing reduces re-render churn. Server-side debouncing also kills request
41
+ storms.
42
+
43
+ ```svelte
44
+ <script lang="ts">
45
+ import { createDebouncer } from '@tanstack/svelte-pacer/debouncer'
46
+ import type { Column } from '@tanstack/svelte-table'
47
+
48
+ type Props = { column: Column<typeof _features, Person> }
49
+ let { column }: Props = $props()
50
+
51
+ let localValue = $state((column.getFilterValue() as string) ?? '')
52
+
53
+ const debouncedSetFilter = createDebouncer(
54
+ (value: string) => column.setFilterValue(value),
55
+ { wait: 200 },
56
+ )
57
+
58
+ function onInput(e: Event) {
59
+ const value = (e.target as HTMLInputElement).value
60
+ localValue = value
61
+ debouncedSetFilter.maybeExecute(value)
62
+ }
63
+ </script>
64
+
65
+ <input type="text" value={localValue} oninput={onInput} placeholder="Search…" />
66
+ ```
67
+
68
+ Why the `localValue`? So the input stays snappy (controlled by `$state`) while the table only
69
+ sees the debounced commit.
70
+
71
+ ## Debounce a global filter
72
+
73
+ Same shape, calling `table.setGlobalFilter`:
74
+
75
+ ```svelte
76
+ <script lang="ts">
77
+ import { createDebouncer } from '@tanstack/svelte-pacer/debouncer'
78
+
79
+ let search = $state('')
80
+ const debouncedSetGlobalFilter = createDebouncer(
81
+ (value: string) => table.setGlobalFilter(value),
82
+ { wait: 250 },
83
+ )
84
+ </script>
85
+
86
+ <input
87
+ type="text"
88
+ value={search}
89
+ oninput={(e) => {
90
+ search = (e.target as HTMLInputElement).value
91
+ debouncedSetGlobalFilter.maybeExecute(search)
92
+ }}
93
+ />
94
+ ```
95
+
96
+ ## Throttle column resizing
97
+
98
+ `columnResizingFeature` writes to `columnSizing` continuously. v9 supports `columnResizeMode:
99
+ 'onChange' | 'onEnd'` — the default is `'onChange'` (commit per-frame). For very heavy tables,
100
+ either:
101
+
102
+ 1. Set `columnResizeMode: 'onEnd'` so the commit only happens at pointerup.
103
+ 2. Or, keep `'onChange'` for the visual handle but throttle the side effects you fire off it.
104
+
105
+ Throttle a side effect:
106
+
107
+ ```ts
108
+ import { createThrottler } from '@tanstack/svelte-pacer/throttler'
109
+
110
+ const throttledSaveSizing = createThrottler(
111
+ (sizing: ColumnSizingState) => persistColumnSizingToStorage(sizing),
112
+ { wait: 100 },
113
+ )
114
+
115
+ $effect(() => {
116
+ const sizing = table.atoms.columnSizing.get()
117
+ throttledSaveSizing.maybeExecute(sizing)
118
+ })
119
+ ```
120
+
121
+ ## Patterns to avoid
122
+
123
+ ### Don't debounce inside a `$effect`
124
+
125
+ ```ts
126
+ // WRONG — new debouncer instance every effect re-run
127
+ $effect(() => {
128
+ const d = createDebouncer((v) => column.setFilterValue(v), { wait: 200 })
129
+ d.maybeExecute(value)
130
+ })
131
+ ```
132
+
133
+ Pacer instances must be stable. Declare them once at component init scope.
134
+
135
+ ### Don't debounce things that should be immediate
136
+
137
+ Selection toggles, page changes, sort clicks — these are user-driven discrete events. Don't
138
+ debounce them. The user expects instant feedback.
139
+
140
+ ### Don't double-commit
141
+
142
+ ```ts
143
+ // WRONG — local state syncs immediately AND the debounced commit fires
144
+ oninput={(e) => {
145
+ column.setFilterValue(e.currentTarget.value)
146
+ debouncedSetFilter.maybeExecute(e.currentTarget.value)
147
+ }}
148
+ ```
149
+
150
+ Pick one. If you want a snappy input, store the input value in local `$state` and only call
151
+ the debounced commit.
152
+
153
+ ## Coordinating with TanStack Query
154
+
155
+ If your filter triggers a server query, debouncing the filter handler is necessary but not
156
+ sufficient — `placeholderData: keepPreviousData` is what keeps the UI from flashing. See the
157
+ `compose-with-tanstack-query` skill.
158
+
159
+ ## Common failure modes
160
+
161
+ - **Recreating pacer instances inside `$effect` / `$derived`.** Each re-run produces a fresh
162
+ debouncer; nothing is ever delayed.
163
+ - **Hand-rolled `setTimeout` debounce.** Loses leading-edge / trailing-edge guarantees; harder
164
+ to cancel on unmount. Use pacer.
165
+ - **Debouncing a value that drives layout.** Causes user-visible lag where there shouldn't
166
+ be. Keep the input controlled by local state and only debounce the table-write call.
167
+ - **Forgetting to call `.maybeExecute`.** Constructing a debouncer doesn't enqueue anything;
168
+ you have to call `.maybeExecute(value)` per event.
169
+ - **Reimplementing pacer with `$effect` timers.** Don't.
170
+
171
+ ## Related skills
172
+
173
+ - `tanstack-table/core/filtering` — column / global filter mechanics.
174
+ - `tanstack-table/core/column-layout` — column resizing modes (`onChange` vs `onEnd`).
175
+ - `tanstack-table/svelte/compose-with-tanstack-query` — pairing pacer with server queries.
176
+ - `tanstack-table/svelte/production-readiness` — where pacer fits in the perf checklist.
@@ -0,0 +1,299 @@
1
+ ---
2
+ name: svelte/compose-with-tanstack-query
3
+ description: >
4
+ Server-side / async data flow with `@tanstack/svelte-query` and `@tanstack/svelte-table`.
5
+ Key the `createQuery` on the table state that drives the request (pagination + sort +
6
+ filters), pass `placeholderData: keepPreviousData` to avoid a "0 rows flash" between pages,
7
+ set `manualPagination` (and optionally `manualSorting` / `manualFiltering`), supply
8
+ `rowCount`, and feed the query result through reactive getters (`get data()`,
9
+ `get rowCount()`). Own driver state with `$state` or `@tanstack/svelte-store` atoms.
10
+ Svelte 5+ only.
11
+ type: composition
12
+ library: tanstack-table
13
+ framework: svelte
14
+ library_version: '9.0.0-alpha.48'
15
+ requires:
16
+ - svelte/client-to-server
17
+ - pagination
18
+ - state-management
19
+ sources:
20
+ - TanStack/table:examples/svelte/with-tanstack-query/
21
+ - TanStack/table:docs/framework/svelte/guide/table-state.md
22
+ ---
23
+
24
+ # Compose with TanStack Query (Svelte)
25
+
26
+ `@tanstack/svelte-query` and `@tanstack/svelte-table` complement each other naturally:
27
+
28
+ - **Query** owns server data — fetching, caching, retries, placeholder data.
29
+ - **Table** owns view state — pagination, sort, filters, selection.
30
+
31
+ The integration is short and predictable: drive the query key from the view state, manual-mode
32
+ the affected pipeline stages, pipe the result back through reactive getters.
33
+
34
+ ## The pattern in 30 seconds
35
+
36
+ ```svelte
37
+ <script lang="ts">
38
+ import { createQuery, keepPreviousData } from '@tanstack/svelte-query'
39
+ import {
40
+ createTable,
41
+ rowPaginationFeature,
42
+ tableFeatures,
43
+ type PaginationState,
44
+ } from '@tanstack/svelte-table'
45
+
46
+ const _features = tableFeatures({ rowPaginationFeature })
47
+
48
+ let pagination: PaginationState = $state({ pageIndex: 0, pageSize: 10 })
49
+
50
+ const dataQuery = createQuery<{
51
+ rows: Array<Person>
52
+ rowCount: number
53
+ }>(() => ({
54
+ queryKey: ['people', pagination],
55
+ queryFn: () => fetchPeople(pagination),
56
+ placeholderData: keepPreviousData,
57
+ }))
58
+
59
+ const table = createTable({
60
+ _features,
61
+ _rowModels: {},
62
+ columns,
63
+ get data() {
64
+ return dataQuery.data?.rows ?? []
65
+ },
66
+ get rowCount() {
67
+ return dataQuery.data?.rowCount
68
+ },
69
+ state: {
70
+ get pagination() {
71
+ return pagination
72
+ },
73
+ },
74
+ onPaginationChange: (updater) => {
75
+ pagination = typeof updater === 'function' ? updater(pagination) : updater
76
+ },
77
+ manualPagination: true,
78
+ })
79
+ </script>
80
+ ```
81
+
82
+ Three things to notice:
83
+
84
+ 1. `queryKey` includes the driver state (`pagination`). Query re-fetches when the page or page
85
+ size changes.
86
+ 2. `placeholderData: keepPreviousData` keeps the previous page visible while the next page
87
+ loads. Without it, `dataQuery.data?.rows` is `undefined` for one tick on every page change
88
+ and the table flashes empty.
89
+ 3. `manualPagination: true` tells the table the data is already paged. Without `rowCount` the
90
+ pager has no idea how many pages exist.
91
+
92
+ ## Driver-state ownership choices
93
+
94
+ You can drive the query from either:
95
+
96
+ - **Component `$state` + `state` + `on[State]Change`** (shown above) — simplest, mirrors
97
+ what most v8 codebases look like after migration.
98
+ - **External `@tanstack/svelte-store` atoms + `atoms`** — preferable when the same state
99
+ drives multiple components (a toolbar, a sidebar, a URL syncer).
100
+
101
+ ```ts
102
+ import { createAtom, useSelector } from '@tanstack/svelte-store'
103
+
104
+ const paginationAtom = createAtom<PaginationState>({
105
+ pageIndex: 0,
106
+ pageSize: 10,
107
+ })
108
+ const pagination = useSelector(paginationAtom)
109
+
110
+ const dataQuery = createQuery(() => ({
111
+ queryKey: ['people', pagination.current],
112
+ queryFn: () => fetchPeople(pagination.current),
113
+ placeholderData: keepPreviousData,
114
+ }))
115
+
116
+ const table = createTable({
117
+ _features,
118
+ _rowModels: {},
119
+ columns,
120
+ get data() {
121
+ return dataQuery.data?.rows ?? []
122
+ },
123
+ get rowCount() {
124
+ return dataQuery.data?.rowCount
125
+ },
126
+ atoms: { pagination: paginationAtom },
127
+ manualPagination: true,
128
+ })
129
+ ```
130
+
131
+ `table.setPageIndex(2)` writes through `paginationAtom`, which invalidates `queryKey`, which
132
+ fetches page 3.
133
+
134
+ ## Adding sort and filters
135
+
136
+ ```ts
137
+ import type { ColumnFiltersState, SortingState } from '@tanstack/svelte-table'
138
+
139
+ let sorting: SortingState = $state([])
140
+ let filters: ColumnFiltersState = $state([])
141
+ let pagination: PaginationState = $state({ pageIndex: 0, pageSize: 10 })
142
+
143
+ const dataQuery = createQuery(() => ({
144
+ queryKey: ['people', pagination, sorting, filters],
145
+ queryFn: () => fetchPeople({ pagination, sorting, filters }),
146
+ placeholderData: keepPreviousData,
147
+ }))
148
+
149
+ const table = createTable({
150
+ _features: tableFeatures({
151
+ rowPaginationFeature,
152
+ rowSortingFeature,
153
+ columnFilteringFeature,
154
+ }),
155
+ _rowModels: {},
156
+ columns,
157
+ get data() {
158
+ return dataQuery.data?.rows ?? []
159
+ },
160
+ get rowCount() {
161
+ return dataQuery.data?.rowCount
162
+ },
163
+ state: {
164
+ get pagination() {
165
+ return pagination
166
+ },
167
+ get sorting() {
168
+ return sorting
169
+ },
170
+ get columnFilters() {
171
+ return filters
172
+ },
173
+ },
174
+ onPaginationChange: (u) =>
175
+ (pagination = typeof u === 'function' ? u(pagination) : u),
176
+ onSortingChange: (u) => (sorting = typeof u === 'function' ? u(sorting) : u),
177
+ onColumnFiltersChange: (u) =>
178
+ (filters = typeof u === 'function' ? u(filters) : u),
179
+ manualPagination: true,
180
+ manualSorting: true,
181
+ manualFiltering: true,
182
+ })
183
+ ```
184
+
185
+ ## Reset page on filter / sort change
186
+
187
+ Otherwise a user filters from "all 5000 people" to "5 named Alice" and stays on page 12.
188
+
189
+ ```ts
190
+ $effect(() => {
191
+ // re-run on identity change
192
+ filters
193
+ sorting
194
+ table.setPageIndex(0)
195
+ })
196
+ ```
197
+
198
+ ## Debounce keystroke-driven filters
199
+
200
+ Without debouncing, a search input fires one request per character.
201
+
202
+ ```ts
203
+ import { createDebouncer } from '@tanstack/svelte-pacer/debouncer'
204
+
205
+ const debouncedSetGlobalFilter = createDebouncer(
206
+ (value: string) => table.setGlobalFilter(value),
207
+ { wait: 250 },
208
+ )
209
+ ```
210
+
211
+ ```svelte
212
+ <input
213
+ oninput={(e) => debouncedSetGlobalFilter.maybeExecute(e.currentTarget.value)}
214
+ />
215
+ ```
216
+
217
+ See the `compose-with-tanstack-pacer` skill for the full pacer pattern.
218
+
219
+ ## Loading and empty states
220
+
221
+ `createQuery` exposes `isFetching`, `isPending`, `isError`, `data`. Use them around the
222
+ table, not inside the row loop.
223
+
224
+ ```svelte
225
+ {#if dataQuery.isPending}
226
+ <div>Loading…</div>
227
+ {:else if dataQuery.isError}
228
+ <div>Failed: {dataQuery.error.message}</div>
229
+ {:else}
230
+ <table>...</table>
231
+ {/if}
232
+
233
+ {#if dataQuery.isFetching}
234
+ <small>Refreshing…</small>
235
+ {/if}
236
+ ```
237
+
238
+ `isFetching` is helpful for the "loading next page" indicator while
239
+ `placeholderData: keepPreviousData` still shows the old rows.
240
+
241
+ ## Optimistic updates (when you also mutate)
242
+
243
+ ```ts
244
+ import { createMutation, useQueryClient } from '@tanstack/svelte-query'
245
+
246
+ const queryClient = useQueryClient()
247
+
248
+ const updatePerson = createMutation(() => ({
249
+ mutationFn: (input: Partial<Person>) =>
250
+ fetch(`/api/people/${input.id}`, {
251
+ method: 'PATCH',
252
+ body: JSON.stringify(input),
253
+ }),
254
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['people'] }),
255
+ }))
256
+ ```
257
+
258
+ After the mutation succeeds, `invalidateQueries` re-fetches; `placeholderData` keeps the old
259
+ rows visible during the refresh.
260
+
261
+ ## SvelteKit `load` integration (a sketch)
262
+
263
+ If your table is on a SvelteKit page, `+page.ts` can hydrate the query cache with the first
264
+ page so SSR renders rows immediately. Subsequent pages still go through `createQuery`.
265
+
266
+ ```ts
267
+ // +page.ts
268
+ export const load = async ({ fetch }) => {
269
+ const initial = await fetchPeople({ pageIndex: 0, pageSize: 10 }, fetch)
270
+ return { initial }
271
+ }
272
+ ```
273
+
274
+ ```svelte
275
+ <script lang="ts">
276
+ let { data } = $props()
277
+ // pass data.initial into placeholderData on first render
278
+ </script>
279
+ ```
280
+
281
+ ## Common failure modes
282
+
283
+ - **Forgot `rowCount`.** Pager shows zero pages.
284
+ - **No `placeholderData: keepPreviousData`.** Empty-table flash on every page change.
285
+ - **Forgot `manualPagination: true`.** Table tries to paginate the already-paged window.
286
+ `getPageCount()` returns 1.
287
+ - **Driver state in `queryKey` is stale.** Always pass the current value, not a captured one.
288
+ - **No reset on filter change.** Stays on dead pages.
289
+ - **Plain `data: dataQuery.data?.rows`.** No reactivity — must be a getter.
290
+ - **Re-creating `createQuery` inside `$effect`.** It's a one-time call; create it at component init.
291
+ - **Reimplementing pagination math against `query.data` instead of calling
292
+ `table.nextPage()`.** Don't.
293
+
294
+ ## Related skills
295
+
296
+ - `tanstack-table/svelte/client-to-server` — base server-side pattern (without Query).
297
+ - `tanstack-table/svelte/compose-with-tanstack-store` — atom-based driver state.
298
+ - `tanstack-table/svelte/compose-with-tanstack-pacer` — debounce / throttle high-frequency inputs.
299
+ - `tanstack-table/core/pagination` — manual mode semantics.