@tanstack/vue-table 9.0.0-beta.7 → 9.0.0-beta.71
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 +5 -1
- package/dist/FlexRender.d.ts +1 -2
- package/dist/FlexRender.js +5 -5
- package/dist/createTableHook.d.ts +40 -20
- package/dist/createTableHook.js +10 -24
- package/dist/experimental-worker-plugin.d.ts +1 -0
- package/dist/experimental-worker-plugin.js +3 -0
- package/dist/index.d.ts +2 -2
- package/dist/merge-proxy.js +1 -2
- package/dist/reactivity.js +3 -8
- package/dist/useTable.d.ts +3 -12
- package/dist/useTable.js +7 -11
- package/package.json +14 -22
- package/skills/create-table-hook/SKILL.md +146 -0
- package/skills/getting-started/SKILL.md +144 -0
- package/skills/migrate-v8-to-v9/SKILL.md +185 -0
- package/skills/table-state/SKILL.md +191 -0
- package/skills/with-tanstack-query/SKILL.md +127 -0
- package/skills/with-tanstack-virtual/SKILL.md +116 -0
- package/dist/FlexRender.cjs +0 -80
- package/dist/FlexRender.cjs.map +0 -1
- package/dist/FlexRender.d.cts +0 -63
- package/dist/FlexRender.js.map +0 -1
- package/dist/createTableHook.cjs +0 -194
- package/dist/createTableHook.cjs.map +0 -1
- package/dist/createTableHook.d.cts +0 -135
- package/dist/createTableHook.js.map +0 -1
- package/dist/flex-render.cjs +0 -5
- package/dist/flex-render.d.cts +0 -2
- package/dist/index.cjs +0 -17
- package/dist/index.d.cts +0 -5
- package/dist/merge-proxy.cjs +0 -77
- package/dist/merge-proxy.cjs.map +0 -1
- package/dist/merge-proxy.js.map +0 -1
- package/dist/reactivity.cjs +0 -64
- package/dist/reactivity.cjs.map +0 -1
- package/dist/reactivity.js.map +0 -1
- package/dist/static-functions.cjs +0 -9
- package/dist/static-functions.d.cts +0 -1
- package/dist/useTable.cjs +0 -76
- package/dist/useTable.cjs.map +0 -1
- package/dist/useTable.d.cts +0 -43
- package/dist/useTable.js.map +0 -1
- package/skills/vue/client-to-server/SKILL.md +0 -365
- package/skills/vue/compose-with-tanstack-form/SKILL.md +0 -369
- package/skills/vue/compose-with-tanstack-pacer/SKILL.md +0 -318
- package/skills/vue/compose-with-tanstack-query/SKILL.md +0 -385
- package/skills/vue/compose-with-tanstack-store/SKILL.md +0 -301
- package/skills/vue/compose-with-tanstack-virtual/SKILL.md +0 -340
- package/skills/vue/getting-started/SKILL.md +0 -409
- package/skills/vue/migrate-v8-to-v9/SKILL.md +0 -375
- package/skills/vue/production-readiness/SKILL.md +0 -271
- package/skills/vue/table-state/SKILL.md +0 -403
- package/src/FlexRender.ts +0 -138
- package/src/createTableHook.ts +0 -534
- package/src/flex-render.ts +0 -1
- package/src/index.ts +0 -4
- package/src/merge-proxy.ts +0 -132
- package/src/reactivity.ts +0 -89
- package/src/static-functions.ts +0 -1
- package/src/useTable.ts +0 -181
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: vue/compose-with-tanstack-query
|
|
3
|
-
description: >
|
|
4
|
-
Server-side / async data flow for `@tanstack/vue-table` v9 + `@tanstack/vue-query`. Key the
|
|
5
|
-
`useQuery` query on the table state that drives the request (pagination + sorting + filters).
|
|
6
|
-
Set `manualPagination` / `manualSorting` / `manualFiltering` for slices the server owns.
|
|
7
|
-
Drop the matching `rowModels` factory (don't ship `paginatedRowModel` when the server
|
|
8
|
-
paginates). Pass `rowCount` so `table.getPageCount()` works without all rows in memory. Pass
|
|
9
|
-
`placeholderData: keepPreviousData` to avoid a "0 rows flash" between pages. The cleanest
|
|
10
|
-
wiring uses external `createAtom`s + `options.atoms` so the table writes through to the atom
|
|
11
|
-
and the query refetches with no `on[State]Change` plumbing. The canonical Vue example is at
|
|
12
|
-
`examples/vue/with-tanstack-query/`.
|
|
13
|
-
type: composition
|
|
14
|
-
library: tanstack-table
|
|
15
|
-
framework: vue
|
|
16
|
-
library_version: '9.0.0-alpha.48'
|
|
17
|
-
requires:
|
|
18
|
-
- vue/client-to-server
|
|
19
|
-
- pagination
|
|
20
|
-
- state-management
|
|
21
|
-
sources:
|
|
22
|
-
- examples/vue/with-tanstack-query/src/App.tsx
|
|
23
|
-
- examples/vue/with-tanstack-query/src/fetchData.ts
|
|
24
|
-
- examples/vue/with-tanstack-query/src/useService.ts
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
# Compose @tanstack/vue-table with @tanstack/vue-query
|
|
28
|
-
|
|
29
|
-
## Dependencies
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
pnpm add @tanstack/vue-table @tanstack/vue-query @tanstack/vue-store
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
`@tanstack/vue-store` is the recommended way to own server-controlled state slices
|
|
36
|
-
(pagination / sorting / global filter). It removes the `on[State]Change` plumbing — the table
|
|
37
|
-
writes through to your atom, and the query refetches because the atom drives `queryKey`.
|
|
38
|
-
|
|
39
|
-
## Setup — paginated server table
|
|
40
|
-
|
|
41
|
-
```vue
|
|
42
|
-
<script setup lang="ts">
|
|
43
|
-
import { computed, ref, watchEffect } from 'vue'
|
|
44
|
-
import { createAtom, useSelector } from '@tanstack/vue-store'
|
|
45
|
-
import { keepPreviousData, useQuery } from '@tanstack/vue-query'
|
|
46
|
-
import {
|
|
47
|
-
FlexRender,
|
|
48
|
-
createColumnHelper,
|
|
49
|
-
rowPaginationFeature,
|
|
50
|
-
tableFeatures,
|
|
51
|
-
useTable,
|
|
52
|
-
type PaginationState,
|
|
53
|
-
} from '@tanstack/vue-table'
|
|
54
|
-
import { fetchPeople } from './fetchData'
|
|
55
|
-
|
|
56
|
-
type Person = { firstName: string; lastName: string; age: number }
|
|
57
|
-
|
|
58
|
-
const features = tableFeatures({ rowPaginationFeature })
|
|
59
|
-
const columnHelper = createColumnHelper<typeof features, Person>()
|
|
60
|
-
const columns = columnHelper.columns([
|
|
61
|
-
columnHelper.accessor('firstName', { header: 'First' }),
|
|
62
|
-
columnHelper.accessor('age', { header: 'Age' }),
|
|
63
|
-
])
|
|
64
|
-
|
|
65
|
-
// 1) Own pagination state in an atom.
|
|
66
|
-
const paginationAtom = createAtom<PaginationState>({
|
|
67
|
-
pageIndex: 0,
|
|
68
|
-
pageSize: 10,
|
|
69
|
-
})
|
|
70
|
-
const pagination = useSelector(paginationAtom)
|
|
71
|
-
|
|
72
|
-
// 2) Vue Query: function-returning-options form ensures reactivity over pagination.value.
|
|
73
|
-
const dataQuery = useQuery(() => ({
|
|
74
|
-
queryKey: ['people', pagination.value],
|
|
75
|
-
queryFn: () => fetchPeople(pagination.value),
|
|
76
|
-
placeholderData: keepPreviousData,
|
|
77
|
-
}))
|
|
78
|
-
|
|
79
|
-
// 3) Stable empty array — avoid `?? []` inline in options.
|
|
80
|
-
const EMPTY: Person[] = []
|
|
81
|
-
const tableData = computed<Person[]>(() => dataQuery.data.value?.rows ?? EMPTY)
|
|
82
|
-
|
|
83
|
-
// 4) Hold last known total so the pager doesn't reset during refetches.
|
|
84
|
-
const rowCount = ref(0)
|
|
85
|
-
watchEffect(() => {
|
|
86
|
-
const next = dataQuery.data.value?.rowCount
|
|
87
|
-
if (next != null) rowCount.value = next
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
// 5) Manual pagination. No paginatedRowModel — server paginates.
|
|
91
|
-
const table = useTable({
|
|
92
|
-
features,
|
|
93
|
-
rowModels: {},
|
|
94
|
-
columns,
|
|
95
|
-
data: tableData,
|
|
96
|
-
rowCount,
|
|
97
|
-
atoms: { pagination: paginationAtom },
|
|
98
|
-
manualPagination: true,
|
|
99
|
-
// NOTE: no `onPaginationChange` — table.setPageIndex writes through to paginationAtom.
|
|
100
|
-
})
|
|
101
|
-
</script>
|
|
102
|
-
|
|
103
|
-
<template>
|
|
104
|
-
<div>
|
|
105
|
-
<table>
|
|
106
|
-
<thead>
|
|
107
|
-
<tr v-for="hg in table.getHeaderGroups()" :key="hg.id">
|
|
108
|
-
<th v-for="h in hg.headers" :key="h.id">
|
|
109
|
-
<FlexRender v-if="!h.isPlaceholder" :header="h" />
|
|
110
|
-
</th>
|
|
111
|
-
</tr>
|
|
112
|
-
</thead>
|
|
113
|
-
<tbody>
|
|
114
|
-
<tr v-for="row in table.getRowModel().rows" :key="row.id">
|
|
115
|
-
<td v-for="cell in row.getAllCells()" :key="cell.id">
|
|
116
|
-
<FlexRender :cell="cell" />
|
|
117
|
-
</td>
|
|
118
|
-
</tr>
|
|
119
|
-
</tbody>
|
|
120
|
-
</table>
|
|
121
|
-
|
|
122
|
-
<button
|
|
123
|
-
@click="table.previousPage()"
|
|
124
|
-
:disabled="!table.getCanPreviousPage()"
|
|
125
|
-
>
|
|
126
|
-
‹
|
|
127
|
-
</button>
|
|
128
|
-
<span
|
|
129
|
-
>Page {{ pagination.pageIndex + 1 }} of {{ table.getPageCount() }}</span
|
|
130
|
-
>
|
|
131
|
-
<button @click="table.nextPage()" :disabled="!table.getCanNextPage()">
|
|
132
|
-
›
|
|
133
|
-
</button>
|
|
134
|
-
<span v-if="dataQuery.isFetching.value">Loading…</span>
|
|
135
|
-
</div>
|
|
136
|
-
</template>
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
Source: `examples/vue/with-tanstack-query/src/App.tsx`.
|
|
140
|
-
|
|
141
|
-
## Core Patterns
|
|
142
|
-
|
|
143
|
-
### 1. `useQuery` accepts a getter for reactive keys
|
|
144
|
-
|
|
145
|
-
```ts
|
|
146
|
-
// ✅ The function form re-evaluates whenever its reactive reads (pagination.value) change.
|
|
147
|
-
const dataQuery = useQuery(() => ({
|
|
148
|
-
queryKey: ['people', pagination.value],
|
|
149
|
-
queryFn: () => fetchPeople(pagination.value),
|
|
150
|
-
placeholderData: keepPreviousData,
|
|
151
|
-
}))
|
|
152
|
-
|
|
153
|
-
// ❌ Object form with raw value — captured once, never refetches on pagination changes.
|
|
154
|
-
const dataQuery = useQuery({
|
|
155
|
-
queryKey: ['people', pagination.value],
|
|
156
|
-
queryFn: () => fetchPeople(pagination.value),
|
|
157
|
-
})
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Vue Query's reactive options require the function form. This is unlike React Query's object
|
|
161
|
-
form — a common transcription error from copy-pasting React examples.
|
|
162
|
-
|
|
163
|
-
### 2. Why external atoms beat `state` + `on*Change` for Query
|
|
164
|
-
|
|
165
|
-
External atoms collapse two flows into one:
|
|
166
|
-
|
|
167
|
-
```ts
|
|
168
|
-
// Atom flow
|
|
169
|
-
table.nextPage() // table writes to atom
|
|
170
|
-
→ paginationAtom.set( ... )
|
|
171
|
-
→ useSelector ref updates
|
|
172
|
-
→ queryKey changes
|
|
173
|
-
→ useQuery refetches
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
```ts
|
|
177
|
-
// state + on*Change flow (still supported; works fine)
|
|
178
|
-
table.nextPage()
|
|
179
|
-
→ onPaginationChange(updater)
|
|
180
|
-
→ setLocalPagination(updater)
|
|
181
|
-
→ ref updates
|
|
182
|
-
→ queryKey changes
|
|
183
|
-
→ useQuery refetches
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
Same end-to-end, but the atom version doesn't need `onPaginationChange` plumbing and a
|
|
187
|
-
sibling component can `useSelector(paginationAtom)` directly without prop-drilling.
|
|
188
|
-
|
|
189
|
-
### 3. Multiple server-controlled slices
|
|
190
|
-
|
|
191
|
-
```ts
|
|
192
|
-
const sortingAtom = createAtom<SortingState>([])
|
|
193
|
-
const paginationAtom = createAtom<PaginationState>({
|
|
194
|
-
pageIndex: 0,
|
|
195
|
-
pageSize: 10,
|
|
196
|
-
})
|
|
197
|
-
const filtersAtom = createAtom<ColumnFiltersState>([])
|
|
198
|
-
|
|
199
|
-
const sorting = useSelector(sortingAtom)
|
|
200
|
-
const pagination = useSelector(paginationAtom)
|
|
201
|
-
const filters = useSelector(filtersAtom)
|
|
202
|
-
|
|
203
|
-
const dataQuery = useQuery(() => ({
|
|
204
|
-
queryKey: [
|
|
205
|
-
'people',
|
|
206
|
-
{
|
|
207
|
-
sorting: sorting.value,
|
|
208
|
-
pagination: pagination.value,
|
|
209
|
-
filters: filters.value,
|
|
210
|
-
},
|
|
211
|
-
],
|
|
212
|
-
queryFn: () =>
|
|
213
|
-
fetchPeople({
|
|
214
|
-
sorting: sorting.value,
|
|
215
|
-
pagination: pagination.value,
|
|
216
|
-
filters: filters.value,
|
|
217
|
-
}),
|
|
218
|
-
placeholderData: keepPreviousData,
|
|
219
|
-
}))
|
|
220
|
-
|
|
221
|
-
const table = useTable({
|
|
222
|
-
features: tableFeatures({
|
|
223
|
-
rowSortingFeature,
|
|
224
|
-
rowPaginationFeature,
|
|
225
|
-
columnFilteringFeature,
|
|
226
|
-
}),
|
|
227
|
-
rowModels: {}, // server owns all three
|
|
228
|
-
columns,
|
|
229
|
-
data: computed(() => dataQuery.data.value?.rows ?? EMPTY),
|
|
230
|
-
rowCount,
|
|
231
|
-
atoms: {
|
|
232
|
-
sorting: sortingAtom,
|
|
233
|
-
pagination: paginationAtom,
|
|
234
|
-
columnFilters: filtersAtom,
|
|
235
|
-
},
|
|
236
|
-
manualSorting: true,
|
|
237
|
-
manualPagination: true,
|
|
238
|
-
manualFiltering: true,
|
|
239
|
-
})
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### 4. Mutations + `invalidateQueries`
|
|
243
|
-
|
|
244
|
-
```ts
|
|
245
|
-
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
|
246
|
-
|
|
247
|
-
const queryClient = useQueryClient()
|
|
248
|
-
|
|
249
|
-
const addPerson = useMutation({
|
|
250
|
-
mutationFn: (person: Person) => savePerson(person),
|
|
251
|
-
onSuccess: () => {
|
|
252
|
-
queryClient.invalidateQueries({ queryKey: ['people'] })
|
|
253
|
-
},
|
|
254
|
-
})
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
The table is a downstream consumer — it has no way to know the server data changed unless
|
|
258
|
-
Query tells it. Always invalidate on writes.
|
|
259
|
-
|
|
260
|
-
## Common Mistakes
|
|
261
|
-
|
|
262
|
-
### Omitting `manualPagination` / `manualSorting` / `manualFiltering` (CRITICAL)
|
|
263
|
-
|
|
264
|
-
```ts
|
|
265
|
-
// ❌ Table re-paginates the already-paginated server response.
|
|
266
|
-
useTable({
|
|
267
|
-
features,
|
|
268
|
-
rowModels: {},
|
|
269
|
-
columns,
|
|
270
|
-
data: tableData,
|
|
271
|
-
rowCount,
|
|
272
|
-
atoms: { pagination: paginationAtom },
|
|
273
|
-
// missing: manualPagination: true
|
|
274
|
-
})
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
The visible symptom is "Page 1 of 1, but I see 10 rows of a 1000-row dataset" — the table
|
|
278
|
-
slices the 10-row page locally.
|
|
279
|
-
|
|
280
|
-
### Missing `rowCount` (CRITICAL)
|
|
281
|
-
|
|
282
|
-
`getPageCount()` falls back to `Math.ceil(data.length / pageSize)` — which is `1` if the
|
|
283
|
-
server already paginated. The pager locks at "Page 1 of 1".
|
|
284
|
-
|
|
285
|
-
### Leaving `paginatedRowModel` registered with `manualPagination` (HIGH)
|
|
286
|
-
|
|
287
|
-
```ts
|
|
288
|
-
// ❌ Ships the factory for nothing AND it tries to paginate server-paginated data.
|
|
289
|
-
rowModels: {
|
|
290
|
-
paginatedRowModel: createPaginatedRowModel()
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// ✅
|
|
294
|
-
rowModels: {
|
|
295
|
-
}
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### Forgetting controlled state in `queryKey` (CRITICAL)
|
|
299
|
-
|
|
300
|
-
```ts
|
|
301
|
-
// ❌ Never refetches when the user pages.
|
|
302
|
-
useQuery(() => ({
|
|
303
|
-
queryKey: ['people'],
|
|
304
|
-
queryFn: () => fetchPeople(pagination.value),
|
|
305
|
-
}))
|
|
306
|
-
|
|
307
|
-
// ✅
|
|
308
|
-
useQuery(() => ({
|
|
309
|
-
queryKey: ['people', pagination.value],
|
|
310
|
-
queryFn: () => fetchPeople(pagination.value),
|
|
311
|
-
}))
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### Skipping `placeholderData: keepPreviousData` (HIGH)
|
|
315
|
-
|
|
316
|
-
Between fetches the table collapses to 0 rows, the row container collapses, scroll position
|
|
317
|
-
jumps. `keepPreviousData` keeps the previous page visible during the refetch.
|
|
318
|
-
|
|
319
|
-
### Passing `useQuery({...})` object form with reactive deps (HIGH — Vue-specific)
|
|
320
|
-
|
|
321
|
-
`@tanstack/vue-query` requires the function form `useQuery(() => ({ ... }))` for reactive
|
|
322
|
-
options. The object form snapshots once and never re-evaluates `queryKey`.
|
|
323
|
-
|
|
324
|
-
### Inline `?? []` in `data` option (MEDIUM)
|
|
325
|
-
|
|
326
|
-
```ts
|
|
327
|
-
// ❌ Fresh array identity every recompute → table option diff churns.
|
|
328
|
-
data: computed(() => dataQuery.data.value?.rows ?? [])
|
|
329
|
-
|
|
330
|
-
// ✅
|
|
331
|
-
const EMPTY: Person[] = []
|
|
332
|
-
const tableData = computed(() => dataQuery.data.value?.rows ?? EMPTY)
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
### Passing the same slice via both `state` and `atoms` (HIGH)
|
|
336
|
-
|
|
337
|
-
`atoms` wins; `state` is silently ignored. Pick one mechanism per slice.
|
|
338
|
-
|
|
339
|
-
### Hallucinating React Query in a Vue project (CRITICAL — top AI tell)
|
|
340
|
-
|
|
341
|
-
```ts
|
|
342
|
-
// ❌ React Query — wrong package for Vue.
|
|
343
|
-
import { useQuery, keepPreviousData } from '@tanstack/react-query'
|
|
344
|
-
|
|
345
|
-
// ✅
|
|
346
|
-
import { useQuery, keepPreviousData } from '@tanstack/vue-query'
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
Same hooks names, different reactivity model (function-returning-options instead of object).
|
|
350
|
-
|
|
351
|
-
### Hallucinating pre-v9 table APIs (CRITICAL)
|
|
352
|
-
|
|
353
|
-
`useVueTable` + `getCoreRowModel: getCoreRowModel()` is v8. v9 uses `useTable` +
|
|
354
|
-
`features` + `rowModels`. See `tanstack-table/vue/migrate-v8-to-v9`.
|
|
355
|
-
|
|
356
|
-
### "API missing" because feature not in `features` (CRITICAL — v9-specific)
|
|
357
|
-
|
|
358
|
-
Server-side pagination still needs `rowPaginationFeature` in `tableFeatures({...})`. The
|
|
359
|
-
feature gives you `table.nextPage` / `table.setPageIndex` / `table.getPageCount`. The
|
|
360
|
-
`rowModels` factory is what you drop; the feature stays.
|
|
361
|
-
|
|
362
|
-
### Reimplementing pagination loop manually (CRITICAL — #1 AI tell)
|
|
363
|
-
|
|
364
|
-
```ts
|
|
365
|
-
// ❌ Hand-rolled — bypasses table invariants.
|
|
366
|
-
const nextPage = () => {
|
|
367
|
-
paginationAtom.set({
|
|
368
|
-
...paginationAtom.get(),
|
|
369
|
-
pageIndex: paginationAtom.get().pageIndex + 1,
|
|
370
|
-
})
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// ✅ Built-ins.
|
|
374
|
-
table.nextPage()
|
|
375
|
-
table.previousPage()
|
|
376
|
-
table.setPageIndex(0)
|
|
377
|
-
table.setPageSize(25)
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
## See Also
|
|
381
|
-
|
|
382
|
-
- `tanstack-table/vue/client-to-server` — manual modes + `rowCount`
|
|
383
|
-
- `tanstack-table/vue/compose-with-tanstack-store` — external atoms in depth
|
|
384
|
-
- `tanstack-table/vue/table-state` — reactivity model
|
|
385
|
-
- `tanstack-table/table-core/pagination` — `manualPagination` semantics
|
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: vue/compose-with-tanstack-store
|
|
3
|
-
description: >
|
|
4
|
-
`@tanstack/vue-table` v9 is built on TanStack Store. Each state slice (sorting, pagination,
|
|
5
|
-
rowSelection, columnFilters, ...) is a separate atom. Three read surfaces:
|
|
6
|
-
`table.atoms.<slice>` (per-slice readonly), `table.store` (flat readonly view), `table.state`
|
|
7
|
-
(selector output from `useTable`'s second argument). Two write paths: the internal
|
|
8
|
-
`table.baseAtoms.<slice>` OR your own `options.atoms[slice]` if you opt to own it. In Vue, create
|
|
9
|
-
atoms with `createAtom` from `@tanstack/vue-store`, read them with `useSelector` (returns a
|
|
10
|
-
ref-like reactive value) or `computed(() => atom.get())`. The Vue adapter installs
|
|
11
|
-
`vueReactivity()` automatically so atoms back onto `computed`/`shallowRef` and subscriptions use
|
|
12
|
-
`watch(..., { flush: 'sync' })`. Precedence is `atoms[key]` > `state[key]` > internal baseAtom.
|
|
13
|
-
type: composition
|
|
14
|
-
library: tanstack-table
|
|
15
|
-
framework: vue
|
|
16
|
-
library_version: '9.0.0-alpha.48'
|
|
17
|
-
requires:
|
|
18
|
-
- state-management
|
|
19
|
-
sources:
|
|
20
|
-
- docs/framework/vue/guide/table-state.md
|
|
21
|
-
- examples/vue/basic-external-atoms/
|
|
22
|
-
- packages/vue-table/src/useTable.ts
|
|
23
|
-
- packages/vue-table/src/reactivity.ts
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
# Compose @tanstack/vue-table with @tanstack/vue-store
|
|
27
|
-
|
|
28
|
-
## Dependencies
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
pnpm add @tanstack/vue-table @tanstack/vue-store
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
`@tanstack/vue-store` provides `createAtom`, `useSelector`, and the `shallow` comparator. The
|
|
35
|
-
Vue table adapter is built on TanStack Store internally — this skill is about exposing that
|
|
36
|
-
machinery to your own app code via external atoms.
|
|
37
|
-
|
|
38
|
-
## Setup — own a slice, share it across components
|
|
39
|
-
|
|
40
|
-
```vue
|
|
41
|
-
<script setup lang="ts">
|
|
42
|
-
import { ref } from 'vue'
|
|
43
|
-
import { createAtom, useSelector } from '@tanstack/vue-store'
|
|
44
|
-
import {
|
|
45
|
-
FlexRender,
|
|
46
|
-
createColumnHelper,
|
|
47
|
-
createPaginatedRowModel,
|
|
48
|
-
createSortedRowModel,
|
|
49
|
-
rowPaginationFeature,
|
|
50
|
-
rowSortingFeature,
|
|
51
|
-
sortFns,
|
|
52
|
-
tableFeatures,
|
|
53
|
-
useTable,
|
|
54
|
-
type PaginationState,
|
|
55
|
-
type SortingState,
|
|
56
|
-
} from '@tanstack/vue-table'
|
|
57
|
-
|
|
58
|
-
type Person = { firstName: string; lastName: string; age: number }
|
|
59
|
-
|
|
60
|
-
const features = tableFeatures({ rowSortingFeature, rowPaginationFeature })
|
|
61
|
-
const columnHelper = createColumnHelper<typeof features, Person>()
|
|
62
|
-
const columns = columnHelper.columns([
|
|
63
|
-
columnHelper.accessor('firstName', { header: 'First' }),
|
|
64
|
-
columnHelper.accessor('age', { header: 'Age' }),
|
|
65
|
-
])
|
|
66
|
-
|
|
67
|
-
// 1) Module / setup scope — stable atom identity. createAtom returns the same atom every call;
|
|
68
|
-
// do NOT recreate it inside a watcher or render fn.
|
|
69
|
-
const sortingAtom = createAtom<SortingState>([])
|
|
70
|
-
const paginationAtom = createAtom<PaginationState>({
|
|
71
|
-
pageIndex: 0,
|
|
72
|
-
pageSize: 10,
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
// 2) Reactive reads anywhere — sibling components can call useSelector on the same atom.
|
|
76
|
-
const sorting = useSelector(sortingAtom)
|
|
77
|
-
const pagination = useSelector(paginationAtom)
|
|
78
|
-
|
|
79
|
-
const data = ref<Person[]>([])
|
|
80
|
-
|
|
81
|
-
// 3) Pass via `options.atoms`. NO `onSortingChange` / `onPaginationChange` needed —
|
|
82
|
-
// `table.setSorting()` writes through to your atom.
|
|
83
|
-
const table = useTable({
|
|
84
|
-
features,
|
|
85
|
-
rowModels: {
|
|
86
|
-
sortedRowModel: createSortedRowModel(sortFns),
|
|
87
|
-
paginatedRowModel: createPaginatedRowModel(),
|
|
88
|
-
},
|
|
89
|
-
columns,
|
|
90
|
-
data,
|
|
91
|
-
atoms: {
|
|
92
|
-
sorting: sortingAtom,
|
|
93
|
-
pagination: paginationAtom,
|
|
94
|
-
},
|
|
95
|
-
})
|
|
96
|
-
</script>
|
|
97
|
-
|
|
98
|
-
<template>
|
|
99
|
-
<div>
|
|
100
|
-
Page {{ pagination.pageIndex + 1 }}, sorted by
|
|
101
|
-
{{ sorting[0]?.id ?? 'none' }}
|
|
102
|
-
</div>
|
|
103
|
-
</template>
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Source: `examples/vue/basic-external-atoms/src/App.tsx`.
|
|
107
|
-
|
|
108
|
-
## Core Patterns
|
|
109
|
-
|
|
110
|
-
### 1. The three read surfaces — pick the narrowest
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
// (a) Atom — narrowest. Reads/writes one slice.
|
|
114
|
-
table.atoms.sorting.get()
|
|
115
|
-
table.atoms.sorting.set([{ id: 'age', desc: true }])
|
|
116
|
-
useSelector(table.atoms.sorting) // reactive ref-like
|
|
117
|
-
computed(() => table.atoms.sorting.get()) // alternative
|
|
118
|
-
|
|
119
|
-
// (b) Flat store — full snapshot.
|
|
120
|
-
table.state // readonly
|
|
121
|
-
table.state.sorting // current value
|
|
122
|
-
|
|
123
|
-
// (c) useTable selector — typed reactive projection.
|
|
124
|
-
const table = useTable(opts, (s) => ({ sorting: s.sorting }))
|
|
125
|
-
table.state.sorting
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
`table.atoms.<slice>` only contains slices for features registered in `features`. If
|
|
129
|
-
`rowSortingFeature` is not registered, `table.atoms.sorting` is `undefined`.
|
|
130
|
-
|
|
131
|
-
### 2. Atom precedence rules
|
|
132
|
-
|
|
133
|
-
Per slice, the resolution order is:
|
|
134
|
-
|
|
135
|
-
```
|
|
136
|
-
options.atoms[slice] > options.state[slice] > table.baseAtoms[slice]
|
|
137
|
-
(external, you own) (controlled state) (internal default)
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
// ❌ Passing both for the SAME slice. `atoms.sorting` wins; `state.sorting` is silently dead.
|
|
142
|
-
useTable({
|
|
143
|
-
features,
|
|
144
|
-
rowModels: {},
|
|
145
|
-
columns,
|
|
146
|
-
data,
|
|
147
|
-
state: {
|
|
148
|
-
get sorting() {
|
|
149
|
-
return localSorting.value
|
|
150
|
-
},
|
|
151
|
-
}, // ignored
|
|
152
|
-
onSortingChange: setLocalSorting, // ignored
|
|
153
|
-
atoms: { sorting: sortingAtom }, // wins
|
|
154
|
-
})
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
Pick **one mechanism per slice**. Different slices can use different mechanisms freely.
|
|
158
|
-
|
|
159
|
-
### 3. When to choose external atoms vs `state` + `on[State]Change`
|
|
160
|
-
|
|
161
|
-
External atoms are better when:
|
|
162
|
-
|
|
163
|
-
- A sibling component (sidebar, header, breadcrumbs) needs to read the same slice without going
|
|
164
|
-
through `table`.
|
|
165
|
-
- You want to persist the slice (localStorage, URL params) — write a `watch(atom, persist)`
|
|
166
|
-
outside the table.
|
|
167
|
-
- You're integrating with TanStack Query and want `queryKey: ['x', useSelector(atom).value]`
|
|
168
|
-
to drive refetches without `on*Change` plumbing.
|
|
169
|
-
|
|
170
|
-
`state` + `on[State]Change` (with **getter wrappers** on each slice — Vue-specific) is fine
|
|
171
|
-
when:
|
|
172
|
-
|
|
173
|
-
- You're migrating from v8 and want the smallest diff.
|
|
174
|
-
- The slice is already a `ref` you don't want to convert.
|
|
175
|
-
|
|
176
|
-
### 4. Reset is YOUR responsibility for owned atoms
|
|
177
|
-
|
|
178
|
-
```ts
|
|
179
|
-
// ❌ table.reset() does NOT clear external atoms — it only resets internally-owned slices.
|
|
180
|
-
table.reset()
|
|
181
|
-
|
|
182
|
-
// ✅ Reset your atoms yourself.
|
|
183
|
-
sortingAtom.set([])
|
|
184
|
-
paginationAtom.set({ pageIndex: 0, pageSize: 10 })
|
|
185
|
-
table.reset() // optional — for any slices the table owns internally
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
Same applies to per-slice resets: `table.resetSorting()` will update through the atom because
|
|
189
|
-
the atom is the slice's source of truth — but a base-atom reset (`table.resetSorting(true)`)
|
|
190
|
-
only resets internal default state, not your owned atom. Read the source if you're chaining
|
|
191
|
-
resets across mixed ownership.
|
|
192
|
-
|
|
193
|
-
### 5. Persisting an atom outside the table
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
import { watch } from 'vue'
|
|
197
|
-
|
|
198
|
-
const sortingAtom = createAtom<SortingState>(
|
|
199
|
-
JSON.parse(localStorage.getItem('mySort') ?? '[]'),
|
|
200
|
-
)
|
|
201
|
-
const sorting = useSelector(sortingAtom)
|
|
202
|
-
watch(sorting, (s) => localStorage.setItem('mySort', JSON.stringify(s)))
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
The table now writes through `table.setSorting()` → `sortingAtom.set(...)` → the watcher
|
|
206
|
-
persists. No `on*Change` involved.
|
|
207
|
-
|
|
208
|
-
## Common Mistakes
|
|
209
|
-
|
|
210
|
-
### Creating atoms inside a render fn or component body without stable identity (CRITICAL)
|
|
211
|
-
|
|
212
|
-
```vue
|
|
213
|
-
<script setup>
|
|
214
|
-
// ❌ A new atom every component setup → the table re-binds every mount → "state resets"
|
|
215
|
-
// feels random to the user. Only an issue when atoms are created in a reactive scope.
|
|
216
|
-
const sortingAtom = computed(() => createAtom < SortingState > [])
|
|
217
|
-
</script>
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
```vue
|
|
221
|
-
<script setup>
|
|
222
|
-
// ✅ Setup scope is once per component instance — fine.
|
|
223
|
-
const sortingAtom = createAtom < SortingState > []
|
|
224
|
-
|
|
225
|
-
// ✅ Module scope — shared across instances.
|
|
226
|
-
// (declare at top of file outside <script setup>)
|
|
227
|
-
</script>
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### Passing the same slice via both `state` and `atoms` (HIGH)
|
|
231
|
-
|
|
232
|
-
`atoms` wins; the `state` plumbing is silently dead. Pick one mechanism per slice.
|
|
233
|
-
|
|
234
|
-
### Pairing `atoms.sorting` with `onSortingChange` (MEDIUM)
|
|
235
|
-
|
|
236
|
-
```ts
|
|
237
|
-
// ❌ Confusing — the handler never fires usefully because the atom is the writeback.
|
|
238
|
-
useTable({
|
|
239
|
-
// ...
|
|
240
|
-
atoms: { sorting: sortingAtom },
|
|
241
|
-
onSortingChange: (u) => {
|
|
242
|
-
/* dead */
|
|
243
|
-
},
|
|
244
|
-
})
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
When using `atoms` for a slice, drop the matching `on[State]Change`. Subscribe to the atom
|
|
248
|
-
itself (`watch(useSelector(sortingAtom), ...)` or `subscribe`) if you need side effects on change.
|
|
249
|
-
|
|
250
|
-
### Expecting `table.reset()` to clear external atoms (HIGH)
|
|
251
|
-
|
|
252
|
-
```ts
|
|
253
|
-
// ❌
|
|
254
|
-
table.reset() // does not touch your atoms
|
|
255
|
-
|
|
256
|
-
// ✅ Reset what you own.
|
|
257
|
-
sortingAtom.set([])
|
|
258
|
-
paginationAtom.set({ pageIndex: 0, pageSize: 10 })
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
### Reading `table.state.<slice>` in a deeply-nested component when only that slice matters (MEDIUM)
|
|
262
|
-
|
|
263
|
-
```ts
|
|
264
|
-
// ❌ All of table.state is computed and tracked even if the component only cares about rowSelection.
|
|
265
|
-
const table = inject(TABLE)
|
|
266
|
-
const count = computed(() => Object.keys(table.state.rowSelection).length)
|
|
267
|
-
|
|
268
|
-
// ✅ Subscribe to the atom directly.
|
|
269
|
-
const selection = useSelector(table.atoms.rowSelection)
|
|
270
|
-
const count = computed(() => Object.keys(selection.value).length)
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
### Reading `atom.get()` outside a reactive scope (HIGH — Vue-specific)
|
|
274
|
-
|
|
275
|
-
```ts
|
|
276
|
-
// ❌ One-shot read; doesn't track.
|
|
277
|
-
const sorting = sortingAtom.get()
|
|
278
|
-
|
|
279
|
-
// ✅ useSelector returns a reactive ref-like.
|
|
280
|
-
const sorting = useSelector(sortingAtom)
|
|
281
|
-
|
|
282
|
-
// ✅ or wrap in computed.
|
|
283
|
-
const sorting = computed(() => sortingAtom.get())
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
### Hallucinating pre-v9 API names (CRITICAL)
|
|
287
|
-
|
|
288
|
-
`useVueTable`, `table.getState()` — both v8. v9 uses `useTable` and `table.state` /
|
|
289
|
-
`table.state` / `table.atoms.<slice>.get()`. See `tanstack-table/vue/migrate-v8-to-v9`.
|
|
290
|
-
|
|
291
|
-
### "API missing" because feature not in `features` (CRITICAL — v9-specific)
|
|
292
|
-
|
|
293
|
-
`table.atoms.sorting` is `undefined` unless `rowSortingFeature` is registered. The fix isn't to
|
|
294
|
-
fall back to `state` — it's to add the feature to `tableFeatures({...})`.
|
|
295
|
-
|
|
296
|
-
## See Also
|
|
297
|
-
|
|
298
|
-
- `tanstack-table/vue/table-state` — read surfaces and selector reactivity
|
|
299
|
-
- `tanstack-table/vue/client-to-server` — atoms + manual modes wiring
|
|
300
|
-
- `tanstack-table/vue/compose-with-tanstack-query` — atoms in the queryKey
|
|
301
|
-
- `tanstack-table/vue/production-readiness` — `useSelector` per-slice for narrow re-renders
|