@tanstack/vue-table 9.0.0-beta.8 → 9.0.0-beta.80
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 +21 -3
- 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 +6 -10
- 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,369 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: vue/compose-with-tanstack-form
|
|
3
|
-
description: >
|
|
4
|
-
Editable cells with `@tanstack/vue-form` + `@tanstack/vue-table` v9. The table is the layout
|
|
5
|
-
primitive; the form owns state. Wire `data: form.state.values.data` (where `data` is the
|
|
6
|
-
array field) so the table reads from the form. In each column's `cell` renderer use a
|
|
7
|
-
`<form.Field name="data[${row.index}].fieldName">` slot to bind an input. Typing gotcha: if
|
|
8
|
-
your row type has recursive `subRows`, type the form rows as `Omit<Row, 'subRows'>` —
|
|
9
|
-
TanStack Form's `DeepKeys` walks the recursion and hits TS2589. Subscribe to
|
|
10
|
-
`form.state.values.data.length` (not the whole array) to drive row add/remove re-renders.
|
|
11
|
-
Pair with TanStack Pacer for debounced filter inputs on the same screen.
|
|
12
|
-
type: composition
|
|
13
|
-
library: tanstack-table
|
|
14
|
-
framework: vue
|
|
15
|
-
library_version: '9.0.0-alpha.48'
|
|
16
|
-
requires:
|
|
17
|
-
- row-selection
|
|
18
|
-
- column-definitions
|
|
19
|
-
sources:
|
|
20
|
-
- examples/react/with-tanstack-form/
|
|
21
|
-
- packages/vue-table/src/useTable.ts
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
# Compose @tanstack/vue-table with @tanstack/vue-form
|
|
25
|
-
|
|
26
|
-
## Dependencies
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
pnpm add @tanstack/vue-table @tanstack/vue-form
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
`@tanstack/vue-form` exposes `useForm` and the `<Field>` / `form.Field` component pattern. It
|
|
33
|
-
does NOT currently ship a `createFormHook` factory — that's a React-only convenience. In Vue,
|
|
34
|
-
you write component-local `<form.Field>` bindings directly. The canonical example for the
|
|
35
|
-
_shape_ of this pattern lives in `examples/react/with-tanstack-form/`; the Vue translation
|
|
36
|
-
maps the React `<form.AppField>` to Vue's `<form.Field>` slot.
|
|
37
|
-
|
|
38
|
-
## Setup — editable rows
|
|
39
|
-
|
|
40
|
-
```vue
|
|
41
|
-
<script setup lang="ts">
|
|
42
|
-
import { computed, ref } from 'vue'
|
|
43
|
-
import { useForm, useStore } from '@tanstack/vue-form'
|
|
44
|
-
import {
|
|
45
|
-
FlexRender,
|
|
46
|
-
createColumnHelper,
|
|
47
|
-
createPaginatedRowModel,
|
|
48
|
-
rowPaginationFeature,
|
|
49
|
-
tableFeatures,
|
|
50
|
-
useTable,
|
|
51
|
-
} from '@tanstack/vue-table'
|
|
52
|
-
import { makeData } from './makeData'
|
|
53
|
-
|
|
54
|
-
// 1) Critical typing: flatten the row shape if your data is recursive.
|
|
55
|
-
type Person = {
|
|
56
|
-
firstName: string
|
|
57
|
-
lastName: string
|
|
58
|
-
age: number
|
|
59
|
-
subRows?: Person[] // recursive — fine for the table
|
|
60
|
-
}
|
|
61
|
-
type FormRow = Omit<Person, 'subRows'> // <-- avoid TS2589 in form DeepKeys
|
|
62
|
-
|
|
63
|
-
const features = tableFeatures({ rowPaginationFeature })
|
|
64
|
-
const columnHelper = createColumnHelper<typeof features, FormRow>()
|
|
65
|
-
|
|
66
|
-
// 2) Form owns the data array. The table reads it.
|
|
67
|
-
const form = useForm({
|
|
68
|
-
defaultValues: { data: makeData(100) as FormRow[] },
|
|
69
|
-
onSubmit: async ({ value }) => {
|
|
70
|
-
// POST value.data
|
|
71
|
-
},
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
// 3) Subscribe to LENGTH only — every keystroke re-rendering the entire table
|
|
75
|
-
// is the canonical performance trap with form-in-table.
|
|
76
|
-
const dataLength = useStore(form.store, (s) => s.values.data.length)
|
|
77
|
-
|
|
78
|
-
// 4) Columns reference `form` via closure. Re-bind through `form.Field` slot.
|
|
79
|
-
const columns = computed(() =>
|
|
80
|
-
columnHelper.columns([
|
|
81
|
-
columnHelper.accessor('firstName', {
|
|
82
|
-
header: 'First',
|
|
83
|
-
cell: ({ row }) => {
|
|
84
|
-
// Return a function/VNode that <FlexRender> can render. Easier: render
|
|
85
|
-
// <form.Field> in the template by passing row.index out — see template below.
|
|
86
|
-
return row.index
|
|
87
|
-
},
|
|
88
|
-
}),
|
|
89
|
-
columnHelper.accessor('lastName', {
|
|
90
|
-
header: 'Last',
|
|
91
|
-
cell: ({ row }) => row.index,
|
|
92
|
-
}),
|
|
93
|
-
columnHelper.accessor('age', {
|
|
94
|
-
header: 'Age',
|
|
95
|
-
cell: ({ row }) => row.index,
|
|
96
|
-
}),
|
|
97
|
-
]),
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
const table = useTable({
|
|
101
|
-
features,
|
|
102
|
-
rowModels: { paginatedRowModel: createPaginatedRowModel() },
|
|
103
|
-
get columns() {
|
|
104
|
-
return columns.value
|
|
105
|
-
},
|
|
106
|
-
// 5) Critical: read straight from form state. Length subscription drives re-renders.
|
|
107
|
-
get data() {
|
|
108
|
-
return form.state.values.data as FormRow[]
|
|
109
|
-
},
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
void dataLength
|
|
113
|
-
|
|
114
|
-
const addRow = () => {
|
|
115
|
-
form.pushFieldValue('data', {
|
|
116
|
-
firstName: '',
|
|
117
|
-
lastName: '',
|
|
118
|
-
age: 0,
|
|
119
|
-
} as FormRow)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const removeRow = (index: number) => {
|
|
123
|
-
form.removeFieldValue('data', index)
|
|
124
|
-
}
|
|
125
|
-
</script>
|
|
126
|
-
|
|
127
|
-
<template>
|
|
128
|
-
<button @click="addRow">Add row</button>
|
|
129
|
-
|
|
130
|
-
<table>
|
|
131
|
-
<thead>
|
|
132
|
-
<tr v-for="hg in table.getHeaderGroups()" :key="hg.id">
|
|
133
|
-
<th v-for="h in hg.headers" :key="h.id">
|
|
134
|
-
<FlexRender v-if="!h.isPlaceholder" :header="h" />
|
|
135
|
-
</th>
|
|
136
|
-
</tr>
|
|
137
|
-
</thead>
|
|
138
|
-
<tbody>
|
|
139
|
-
<!-- In Vue, the cleanest way to bind cells is to render <form.Field> in
|
|
140
|
-
the template using row + column id, instead of stuffing JSX into
|
|
141
|
-
the `cell` def. -->
|
|
142
|
-
<tr v-for="row in table.getRowModel().rows" :key="row.id">
|
|
143
|
-
<td>
|
|
144
|
-
<form.Field :name="`data[${row.index}].firstName`">
|
|
145
|
-
<template #default="{ field }">
|
|
146
|
-
<input
|
|
147
|
-
:value="field.state.value"
|
|
148
|
-
@input="(e: any) => field.handleChange(e.target.value)"
|
|
149
|
-
@blur="field.handleBlur"
|
|
150
|
-
/>
|
|
151
|
-
</template>
|
|
152
|
-
</form.Field>
|
|
153
|
-
</td>
|
|
154
|
-
<td>
|
|
155
|
-
<form.Field :name="`data[${row.index}].lastName`">
|
|
156
|
-
<template #default="{ field }">
|
|
157
|
-
<input
|
|
158
|
-
:value="field.state.value"
|
|
159
|
-
@input="(e: any) => field.handleChange(e.target.value)"
|
|
160
|
-
/>
|
|
161
|
-
</template>
|
|
162
|
-
</form.Field>
|
|
163
|
-
</td>
|
|
164
|
-
<td>
|
|
165
|
-
<form.Field :name="`data[${row.index}].age`">
|
|
166
|
-
<template #default="{ field }">
|
|
167
|
-
<input
|
|
168
|
-
type="number"
|
|
169
|
-
:value="field.state.value"
|
|
170
|
-
@input="(e: any) => field.handleChange(Number(e.target.value))"
|
|
171
|
-
/>
|
|
172
|
-
</template>
|
|
173
|
-
</form.Field>
|
|
174
|
-
</td>
|
|
175
|
-
<td><button @click="removeRow(row.index)">×</button></td>
|
|
176
|
-
</tr>
|
|
177
|
-
</tbody>
|
|
178
|
-
</table>
|
|
179
|
-
</template>
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
Source: `examples/react/with-tanstack-form/src/main.tsx` (React canonical); pattern translated
|
|
183
|
-
to Vue's `<form.Field>` slot API.
|
|
184
|
-
|
|
185
|
-
## Core Patterns
|
|
186
|
-
|
|
187
|
-
### 1. Form owns the data, table renders it
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
const form = useForm({ defaultValues: { data: makeData(100) } })
|
|
191
|
-
|
|
192
|
-
const table = useTable({
|
|
193
|
-
features,
|
|
194
|
-
rowModels: { paginatedRowModel: createPaginatedRowModel() },
|
|
195
|
-
columns,
|
|
196
|
-
get data() {
|
|
197
|
-
return form.state.values.data
|
|
198
|
-
},
|
|
199
|
-
})
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
The table is a layout primitive — pagination, sorting, filtering on the form's data. The form
|
|
203
|
-
handles editing, validation, dirty tracking, submit.
|
|
204
|
-
|
|
205
|
-
### 2. Cell bindings via `<form.Field name="data[${row.index}].field">`
|
|
206
|
-
|
|
207
|
-
In React-form, the convention is `<form.AppField name="..." />` inside `cell:`. In Vue-form,
|
|
208
|
-
prefer rendering `<form.Field>` directly in the `<template>` instead of from `cell` — the slot
|
|
209
|
-
API doesn't translate cleanly through `cell: ({ row }) => ...` because cell functions return
|
|
210
|
-
VNodes, not template fragments.
|
|
211
|
-
|
|
212
|
-
If you do return VNodes from `cell`, use `h()`:
|
|
213
|
-
|
|
214
|
-
```ts
|
|
215
|
-
import { h } from 'vue'
|
|
216
|
-
|
|
217
|
-
columnHelper.accessor('firstName', {
|
|
218
|
-
header: 'First',
|
|
219
|
-
cell: ({ row, table: _t }) =>
|
|
220
|
-
h(
|
|
221
|
-
form.Field,
|
|
222
|
-
{ name: `data[${row.index}].firstName` },
|
|
223
|
-
{
|
|
224
|
-
default: ({ field }: any) =>
|
|
225
|
-
h('input', {
|
|
226
|
-
value: field.state.value,
|
|
227
|
-
onInput: (e: any) => field.handleChange(e.target.value),
|
|
228
|
-
}),
|
|
229
|
-
},
|
|
230
|
-
),
|
|
231
|
-
})
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### 3. Length-only subscription to drive add/remove re-renders
|
|
235
|
-
|
|
236
|
-
```ts
|
|
237
|
-
import { useStore } from '@tanstack/vue-form'
|
|
238
|
-
|
|
239
|
-
// ✅ Re-renders only when an item is added/removed — not on every keystroke.
|
|
240
|
-
const dataLength = useStore(form.store, (s) => s.values.data.length)
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
```ts
|
|
244
|
-
// ❌ Subscribing to the whole array re-renders the whole table on every keystroke.
|
|
245
|
-
const allData = useStore(form.store, (s) => s.values.data)
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
The table reads `form.state.values.data` synchronously per render — you don't need the full
|
|
249
|
-
array in a watcher; you just need to trigger a re-render when length changes.
|
|
250
|
-
|
|
251
|
-
### 4. Recursive row types: `Omit<Row, 'subRows'>` for the form
|
|
252
|
-
|
|
253
|
-
```ts
|
|
254
|
-
type Person = { firstName: string; subRows?: Person[] } // recursive — fine for the TABLE
|
|
255
|
-
type FormRow = Omit<Person, 'subRows'> // flat — required for the FORM
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
TanStack Form's `DeepKeys` walks `subRows` recursively and TypeScript hits TS2589 ("type
|
|
259
|
-
instantiation is excessively deep"). Always flatten the row type before passing to the form.
|
|
260
|
-
|
|
261
|
-
### 5. Pair with Pacer for debounced filter inputs on the same page
|
|
262
|
-
|
|
263
|
-
Editable cells re-render on every keystroke; a filter input on top of the same table would
|
|
264
|
-
recompute the row model per character. Wrap the filter writer in a debounced callback — see
|
|
265
|
-
`tanstack-table/vue/compose-with-tanstack-pacer`.
|
|
266
|
-
|
|
267
|
-
## Common Mistakes
|
|
268
|
-
|
|
269
|
-
### Typing rows with recursive `subRows` and feeding to `useForm` (CRITICAL)
|
|
270
|
-
|
|
271
|
-
```ts
|
|
272
|
-
// ❌ TS2589: "Type instantiation is excessively deep and possibly infinite"
|
|
273
|
-
const form = useForm({ defaultValues: { data: makeData(100) as Person[] } })
|
|
274
|
-
|
|
275
|
-
// ✅
|
|
276
|
-
type FormRow = Omit<Person, 'subRows'>
|
|
277
|
-
const form = useForm({ defaultValues: { data: makeData(100) as FormRow[] } })
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
### Subscribing to the entire `form.state.values.data` (HIGH)
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
// ❌ Every keystroke re-renders the entire App.
|
|
284
|
-
const data = useStore(form.store, (s) => s.values.data)
|
|
285
|
-
|
|
286
|
-
// ✅
|
|
287
|
-
const dataLength = useStore(form.store, (s) => s.values.data.length)
|
|
288
|
-
// then in useTable: get data() { return form.state.values.data }
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
### Putting `form` itself in `useTable`'s `data` (HIGH)
|
|
292
|
-
|
|
293
|
-
```ts
|
|
294
|
-
// ❌
|
|
295
|
-
useTable({ ..., data: form })
|
|
296
|
-
|
|
297
|
-
// ✅
|
|
298
|
-
useTable({ ..., get data() { return form.state.values.data } })
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
The table only consumes the rows array — not the form instance.
|
|
302
|
-
|
|
303
|
-
### Returning a `<form.Field>` template fragment from `cell:` (MEDIUM — Vue-specific)
|
|
304
|
-
|
|
305
|
-
Vue templates can't be returned from JS functions — they're compiled to render fns at build
|
|
306
|
-
time. From `cell:`, return either a plain value or a VNode via `h()`. For binding with
|
|
307
|
-
templates, render `<form.Field>` in the `<template>` instead.
|
|
308
|
-
|
|
309
|
-
### Forgetting `useMemo`/`computed` around `columns` when columns close over `form` (HIGH)
|
|
310
|
-
|
|
311
|
-
```ts
|
|
312
|
-
// ❌ New columns array each render → table re-binds → form bindings reset.
|
|
313
|
-
const columns = columnHelper.columns([
|
|
314
|
-
/* refs form */
|
|
315
|
-
])
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
```ts
|
|
319
|
-
// ✅
|
|
320
|
-
const columns = computed(() =>
|
|
321
|
-
columnHelper.columns([
|
|
322
|
-
/* refs form */
|
|
323
|
-
]),
|
|
324
|
-
)
|
|
325
|
-
const table = useTable({
|
|
326
|
-
features,
|
|
327
|
-
rowModels: {},
|
|
328
|
-
get columns() {
|
|
329
|
-
return columns.value
|
|
330
|
-
},
|
|
331
|
-
get data() {
|
|
332
|
-
return form.state.values.data
|
|
333
|
-
},
|
|
334
|
-
})
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
### Reusing the v8 `useReactTable` + `tableMeta.updateData` editable-cell pattern (HIGH)
|
|
338
|
-
|
|
339
|
-
That worked in v8 (and mechanically still does), but the v9-blessed approach is form
|
|
340
|
-
composition. You get validation, dirty tracking, submit, and reset for free.
|
|
341
|
-
|
|
342
|
-
### Hallucinating `createFormHook` in Vue (HIGH — Vue-specific)
|
|
343
|
-
|
|
344
|
-
`createFormHook` is React-form's factory for pre-bound field components. Vue-form does not
|
|
345
|
-
ship it. Use `useForm` + `<form.Field>` slots directly.
|
|
346
|
-
|
|
347
|
-
### "API missing" because feature not in `features` (CRITICAL — v9-specific)
|
|
348
|
-
|
|
349
|
-
The table still needs `tableFeatures({ rowPaginationFeature, … })` for whatever features
|
|
350
|
-
your editable table uses. The form composition doesn't replace that requirement.
|
|
351
|
-
|
|
352
|
-
### Reimplementing form state in `ref`s per cell (CRITICAL — #1 AI tell)
|
|
353
|
-
|
|
354
|
-
```ts
|
|
355
|
-
// ❌ A ref per cell, custom validation, manual dirty tracking…
|
|
356
|
-
const firstNameRefs = new Map<string, Ref<string>>()
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
```ts
|
|
360
|
-
// ✅ Let TanStack Form own it.
|
|
361
|
-
form.state.values.data[row.index].firstName
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
## See Also
|
|
365
|
-
|
|
366
|
-
- `tanstack-table/vue/compose-with-tanstack-pacer` — debounce filter inputs alongside editing
|
|
367
|
-
- `tanstack-table/vue/compose-with-tanstack-store` — share atoms with the form's `form.store`
|
|
368
|
-
- `tanstack-table/table-core/row-selection` — selection + editable rows together
|
|
369
|
-
- `tanstack-table/table-core/column-definitions` — `cell` renderer typing
|
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: vue/compose-with-tanstack-pacer
|
|
3
|
-
description: >
|
|
4
|
-
Use `@tanstack/pacer` to debounce/throttle high-frequency writes in `@tanstack/vue-table` v9:
|
|
5
|
-
column filter inputs and column-resize state. There is no `@tanstack/vue-pacer` adapter yet —
|
|
6
|
-
use the framework-agnostic core `@tanstack/pacer` (`Debouncer`, `Throttler`, or the
|
|
7
|
-
`debounce`/`throttle` function helpers) directly from a Vue component. Pattern: keep local
|
|
8
|
-
state for the input value so typing feels instant; route the table-writing callback through
|
|
9
|
-
a `Debouncer` instance scoped to the component (so cleanup happens on unmount); pick
|
|
10
|
-
`wait: 300` for filter inputs and `wait: 16` (one frame) for column resize. Pacer replaces
|
|
11
|
-
the hand-rolled `setTimeout`-based `DebouncedInput` component that appears in every v8
|
|
12
|
-
filtering example.
|
|
13
|
-
type: composition
|
|
14
|
-
library: tanstack-table
|
|
15
|
-
framework: vue
|
|
16
|
-
library_version: '9.0.0-alpha.48'
|
|
17
|
-
requires:
|
|
18
|
-
- filtering
|
|
19
|
-
- column-layout
|
|
20
|
-
sources:
|
|
21
|
-
- examples/vue/filters/src/DebouncedInput.vue
|
|
22
|
-
- examples/react/with-tanstack-form/
|
|
23
|
-
- packages/pacer/src/debouncer.ts
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
# Compose @tanstack/vue-table with @tanstack/pacer
|
|
27
|
-
|
|
28
|
-
## Dependencies
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
pnpm add @tanstack/vue-table @tanstack/pacer
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
**Note:** there is no `@tanstack/vue-pacer` adapter at the time of writing — only React,
|
|
35
|
-
Preact, Solid, and Angular ship dedicated Pacer adapters. Vue users consume the
|
|
36
|
-
framework-agnostic core (`@tanstack/pacer`) directly. That's a fine fit: `Debouncer` and
|
|
37
|
-
`Throttler` are plain classes; integration with Vue is just `onBeforeUnmount(() => debouncer.cancel())`.
|
|
38
|
-
|
|
39
|
-
## Setup — debounced filter input
|
|
40
|
-
|
|
41
|
-
```vue
|
|
42
|
-
<!-- DebouncedInput.vue — the v9 replacement for the hand-rolled v8 component -->
|
|
43
|
-
<script setup lang="ts">
|
|
44
|
-
import { onBeforeUnmount, ref, watch } from 'vue'
|
|
45
|
-
import { Debouncer } from '@tanstack/pacer'
|
|
46
|
-
|
|
47
|
-
const props = defineProps<{
|
|
48
|
-
modelValue: string | number
|
|
49
|
-
debounce?: number
|
|
50
|
-
}>()
|
|
51
|
-
const emit = defineEmits<{ 'update:modelValue': [value: string | number] }>()
|
|
52
|
-
|
|
53
|
-
// 1) Local state — instant UI updates while the user types.
|
|
54
|
-
const local = ref(props.modelValue)
|
|
55
|
-
|
|
56
|
-
// 2) One Debouncer per component instance. Reset between renders is not needed.
|
|
57
|
-
const debouncer = new Debouncer(
|
|
58
|
-
(next: string | number) => emit('update:modelValue', next),
|
|
59
|
-
{ wait: props.debounce ?? 300 },
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
// 3) Keep local in sync with external changes (e.g. table.resetColumnFilters()).
|
|
63
|
-
watch(
|
|
64
|
-
() => props.modelValue,
|
|
65
|
-
(v) => {
|
|
66
|
-
local.value = v
|
|
67
|
-
},
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
const onInput = (e: Event) => {
|
|
71
|
-
const v = (e.target as HTMLInputElement).value
|
|
72
|
-
local.value = v
|
|
73
|
-
debouncer.maybeExecute(v)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// 4) Cancel pending work on unmount.
|
|
77
|
-
onBeforeUnmount(() => debouncer.cancel())
|
|
78
|
-
</script>
|
|
79
|
-
|
|
80
|
-
<template>
|
|
81
|
-
<input :value="local" @input="onInput" />
|
|
82
|
-
</template>
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Use it in a column filter:
|
|
86
|
-
|
|
87
|
-
```vue
|
|
88
|
-
<DebouncedInput
|
|
89
|
-
:model-value="(column.getFilterValue() ?? '') as string"
|
|
90
|
-
@update:model-value="(v) => column.setFilterValue(v)"
|
|
91
|
-
placeholder="Search…"
|
|
92
|
-
:debounce="300"
|
|
93
|
-
/>
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Core Patterns
|
|
97
|
-
|
|
98
|
-
### 1. Local state + debounced writer = instant input, deferred store write
|
|
99
|
-
|
|
100
|
-
```ts
|
|
101
|
-
// ❌ Writes to the table on every keystroke. Row model recomputes per character.
|
|
102
|
-
@input="(e) => column.setFilterValue(e.target.value)"
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
```ts
|
|
106
|
-
// ✅ Local ref drives the input; Debouncer batches the store write at wait=300.
|
|
107
|
-
const local = ref('')
|
|
108
|
-
const debouncer = new Debouncer((v) => column.setFilterValue(v), { wait: 300 })
|
|
109
|
-
const onInput = (e) => {
|
|
110
|
-
local.value = e.target.value
|
|
111
|
-
debouncer.maybeExecute(local.value)
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### 2. `Debouncer` vs `Throttler`
|
|
116
|
-
|
|
117
|
-
| Use case | Pick | Typical `wait` |
|
|
118
|
-
| ---------------------------------------- | ----------- | ------------------- |
|
|
119
|
-
| Filter input (commit after typing stops) | `Debouncer` | `300` ms |
|
|
120
|
-
| Global filter (same shape) | `Debouncer` | `250–500` ms |
|
|
121
|
-
| Column resize drag | `Throttler` | `16` ms (one frame) |
|
|
122
|
-
| Scroll-triggered fetch | `Throttler` | `100–250` ms |
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
import { Throttler } from '@tanstack/pacer'
|
|
126
|
-
|
|
127
|
-
const resizeThrottler = new Throttler(
|
|
128
|
-
(event: ColumnResizeInfoEvent) => /* commit resize */,
|
|
129
|
-
{ wait: 16 },
|
|
130
|
-
)
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### 3. Throttling column resize via the atoms write path
|
|
134
|
-
|
|
135
|
-
If you own column resize state externally (via `@tanstack/vue-store`), throttle the atom
|
|
136
|
-
write:
|
|
137
|
-
|
|
138
|
-
```ts
|
|
139
|
-
import { Throttler } from '@tanstack/pacer'
|
|
140
|
-
import { createAtom } from '@tanstack/vue-store'
|
|
141
|
-
import type { ColumnResizingState } from '@tanstack/vue-table'
|
|
142
|
-
|
|
143
|
-
const columnResizingAtom = createAtom<ColumnResizingState>(
|
|
144
|
-
{} as ColumnResizingState,
|
|
145
|
-
)
|
|
146
|
-
const throttler = new Throttler(
|
|
147
|
-
(next: ColumnResizingState) => columnResizingAtom.set(next),
|
|
148
|
-
{ wait: 16 },
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
useTable({
|
|
152
|
-
features,
|
|
153
|
-
rowModels: {},
|
|
154
|
-
columns,
|
|
155
|
-
data,
|
|
156
|
-
state: {
|
|
157
|
-
get columnResizing() {
|
|
158
|
-
return columnResizingAtom.get()
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
onColumnResizingChange: (u) => {
|
|
162
|
-
const next = typeof u === 'function' ? u(columnResizingAtom.get()) : u
|
|
163
|
-
throttler.maybeExecute(next)
|
|
164
|
-
},
|
|
165
|
-
})
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
For pure local resize state, the same pattern using `ref` + `onColumnResizingChange` works.
|
|
169
|
-
|
|
170
|
-
### 4. Global filter debounce
|
|
171
|
-
|
|
172
|
-
```vue
|
|
173
|
-
<script setup lang="ts">
|
|
174
|
-
import { onBeforeUnmount, ref, watch } from 'vue'
|
|
175
|
-
import { Debouncer } from '@tanstack/pacer'
|
|
176
|
-
|
|
177
|
-
const props = defineProps<{ table: any }>() // import the typed Table<TFeatures, TData> in real code
|
|
178
|
-
const local = ref('')
|
|
179
|
-
|
|
180
|
-
const debouncer = new Debouncer((v: string) => props.table.setGlobalFilter(v), {
|
|
181
|
-
wait: 250,
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
watch(local, (v) => debouncer.maybeExecute(v))
|
|
185
|
-
onBeforeUnmount(() => debouncer.cancel())
|
|
186
|
-
</script>
|
|
187
|
-
|
|
188
|
-
<template>
|
|
189
|
-
<input v-model="local" placeholder="Search all columns…" />
|
|
190
|
-
</template>
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
### 5. The `debounce`/`throttle` function helpers (alternative shape)
|
|
194
|
-
|
|
195
|
-
If you don't need access to the instance (`cancel`, `flush`, etc.), the function helpers are
|
|
196
|
-
the lowest-overhead form:
|
|
197
|
-
|
|
198
|
-
```ts
|
|
199
|
-
import { debounce } from '@tanstack/pacer'
|
|
200
|
-
|
|
201
|
-
const writeFilter = debounce((v: string) => column.setFilterValue(v), {
|
|
202
|
-
wait: 300,
|
|
203
|
-
})
|
|
204
|
-
// later: writeFilter(value)
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
They still register internally for proper cleanup via the Pacer event client; you just don't
|
|
208
|
-
expose a handle.
|
|
209
|
-
|
|
210
|
-
## Common Mistakes
|
|
211
|
-
|
|
212
|
-
### Writing `column.setFilterValue` directly on every keystroke (HIGH)
|
|
213
|
-
|
|
214
|
-
```ts
|
|
215
|
-
// ❌ Whole filtered row model recomputes per character.
|
|
216
|
-
@input="(e) => column.setFilterValue(e.target.value)"
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
Fine for 100 rows, miserable for 10k+. Debounce the writer.
|
|
220
|
-
|
|
221
|
-
### Hand-rolling `setTimeout`-based debounce (HIGH)
|
|
222
|
-
|
|
223
|
-
```ts
|
|
224
|
-
// ❌ The v8-era DebouncedInput pattern — works, but reinvents what Pacer does correctly.
|
|
225
|
-
const t = ref<ReturnType<typeof setTimeout>>()
|
|
226
|
-
const setter = (v) => {
|
|
227
|
-
if (t.value) clearTimeout(t.value)
|
|
228
|
-
t.value = setTimeout(() => emit('update:modelValue', v), 300)
|
|
229
|
-
}
|
|
230
|
-
onBeforeUnmount(() => clearTimeout(t.value))
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
Pacer handles `cancel`/`flush`/`maybeExecute`/leading edge/trailing edge consistently and
|
|
234
|
-
integrates with the Pacer Devtools event client.
|
|
235
|
-
|
|
236
|
-
### Importing from `@tanstack/vue-pacer` (HIGH — Vue-specific)
|
|
237
|
-
|
|
238
|
-
There is no `@tanstack/vue-pacer` package. Import from `@tanstack/pacer` directly.
|
|
239
|
-
|
|
240
|
-
```ts
|
|
241
|
-
// ❌
|
|
242
|
-
import { useDebouncedCallback } from '@tanstack/vue-pacer'
|
|
243
|
-
|
|
244
|
-
// ✅
|
|
245
|
-
import { Debouncer } from '@tanstack/pacer'
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
### Debouncing the local input state too (MEDIUM)
|
|
249
|
-
|
|
250
|
-
The user sees stale characters in the input. Local state must be **instant**; only the
|
|
251
|
-
table/store write should debounce.
|
|
252
|
-
|
|
253
|
-
```ts
|
|
254
|
-
// ❌
|
|
255
|
-
const debouncedLocal = new Debouncer((v) => (local.value = v), { wait: 300 })
|
|
256
|
-
|
|
257
|
-
// ✅ Local instant, store deferred.
|
|
258
|
-
local.value = e.target.value
|
|
259
|
-
debouncer.maybeExecute(local.value)
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
### Throttling resize at 250ms (MEDIUM)
|
|
263
|
-
|
|
264
|
-
Too long — drag feels laggy. Use `wait: 16` (roughly one animation frame) for resize. 250ms
|
|
265
|
-
is a filter-input wait.
|
|
266
|
-
|
|
267
|
-
### Forgetting `onBeforeUnmount(() => debouncer.cancel())` (MEDIUM)
|
|
268
|
-
|
|
269
|
-
Pending fires after unmount → call into a stale closure / nonexistent table → console error.
|
|
270
|
-
Always cancel on unmount.
|
|
271
|
-
|
|
272
|
-
### Hallucinating `useDebouncedCallback` from React (CRITICAL — top AI tell)
|
|
273
|
-
|
|
274
|
-
`useDebouncedCallback` is a React-pacer hook. It doesn't exist in `@tanstack/vue-pacer` (which
|
|
275
|
-
doesn't exist either) or in core `@tanstack/pacer`. Use the `Debouncer` class or the
|
|
276
|
-
`debounce` function helper in Vue.
|
|
277
|
-
|
|
278
|
-
### Hallucinating pre-v9 table APIs in the writer (CRITICAL)
|
|
279
|
-
|
|
280
|
-
The debounced callback wraps `column.setFilterValue`, `table.setGlobalFilter`,
|
|
281
|
-
`columnResizingAtom.set`, etc. These are v9 APIs — `useVueTable` /
|
|
282
|
-
`table.setGlobalFilter` on a v8 table would not exist. See
|
|
283
|
-
`tanstack-table/vue/migrate-v8-to-v9`.
|
|
284
|
-
|
|
285
|
-
### "Filter API missing" because feature not in `features` (CRITICAL — v9-specific)
|
|
286
|
-
|
|
287
|
-
`column.setFilterValue` is only available if `columnFilteringFeature` is registered.
|
|
288
|
-
`table.setGlobalFilter` requires `globalFilteringFeature`. Debouncing a missing API is still
|
|
289
|
-
a missing API.
|
|
290
|
-
|
|
291
|
-
### Reimplementing per-keystroke filter logic (CRITICAL — #1 AI tell)
|
|
292
|
-
|
|
293
|
-
```ts
|
|
294
|
-
// ❌ Filter rows yourself.
|
|
295
|
-
const filtered = computed(() =>
|
|
296
|
-
data.value.filter((r) => r.name.includes(local.value)),
|
|
297
|
-
)
|
|
298
|
-
useTable({ ..., data: filtered })
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
```ts
|
|
302
|
-
// ✅ Use the table's filter feature; debounce the writer.
|
|
303
|
-
useTable({
|
|
304
|
-
features: tableFeatures({ columnFilteringFeature, globalFilteringFeature }),
|
|
305
|
-
rowModels: { filteredRowModel: createFilteredRowModel(filterFns) },
|
|
306
|
-
columns,
|
|
307
|
-
data,
|
|
308
|
-
})
|
|
309
|
-
// In a filter input:
|
|
310
|
-
debouncer.maybeExecute(local.value) // → table.setGlobalFilter(local.value)
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
## See Also
|
|
314
|
-
|
|
315
|
-
- `tanstack-table/vue/compose-with-tanstack-form` — debounce filter while editing
|
|
316
|
-
- `tanstack-table/vue/production-readiness` — debounce + tree-shake together
|
|
317
|
-
- `tanstack-table/table-core/filtering` — `columnFilteringFeature` / `globalFilteringFeature`
|
|
318
|
-
- `tanstack-table/table-core/column-layout` — column resizing state
|