@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.
- package/README.md +10 -0
- package/package.json +7 -5
- 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,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.
|