@tanstack/vue-table 9.0.0-beta.5 → 9.0.0-beta.50

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.
Files changed (48) hide show
  1. package/README.md +2 -0
  2. package/dist/FlexRender.cjs +2 -3
  3. package/dist/FlexRender.cjs.map +1 -1
  4. package/dist/FlexRender.js +2 -3
  5. package/dist/FlexRender.js.map +1 -1
  6. package/dist/createTableHook.cjs +4 -9
  7. package/dist/createTableHook.cjs.map +1 -1
  8. package/dist/createTableHook.d.cts +39 -12
  9. package/dist/createTableHook.d.ts +39 -12
  10. package/dist/createTableHook.js +4 -9
  11. package/dist/createTableHook.js.map +1 -1
  12. package/dist/experimental-worker-plugin.cjs +9 -0
  13. package/dist/experimental-worker-plugin.d.cts +1 -0
  14. package/dist/experimental-worker-plugin.d.ts +1 -0
  15. package/dist/experimental-worker-plugin.js +3 -0
  16. package/dist/index.d.cts +2 -2
  17. package/dist/index.d.ts +2 -2
  18. package/dist/reactivity.cjs +2 -6
  19. package/dist/reactivity.cjs.map +1 -1
  20. package/dist/reactivity.js +2 -6
  21. package/dist/reactivity.js.map +1 -1
  22. package/dist/useTable.cjs +3 -7
  23. package/dist/useTable.cjs.map +1 -1
  24. package/dist/useTable.d.cts +1 -8
  25. package/dist/useTable.d.ts +1 -8
  26. package/dist/useTable.js +3 -7
  27. package/dist/useTable.js.map +1 -1
  28. package/package.json +8 -4
  29. package/skills/create-table-hook/SKILL.md +146 -0
  30. package/skills/getting-started/SKILL.md +144 -0
  31. package/skills/migrate-v8-to-v9/SKILL.md +185 -0
  32. package/skills/table-state/SKILL.md +191 -0
  33. package/skills/with-tanstack-query/SKILL.md +127 -0
  34. package/skills/with-tanstack-virtual/SKILL.md +116 -0
  35. package/src/createTableHook.ts +108 -17
  36. package/src/experimental-worker-plugin.ts +1 -0
  37. package/src/reactivity.ts +1 -2
  38. package/src/useTable.ts +3 -11
  39. package/skills/vue/client-to-server/SKILL.md +0 -365
  40. package/skills/vue/compose-with-tanstack-form/SKILL.md +0 -369
  41. package/skills/vue/compose-with-tanstack-pacer/SKILL.md +0 -318
  42. package/skills/vue/compose-with-tanstack-query/SKILL.md +0 -385
  43. package/skills/vue/compose-with-tanstack-store/SKILL.md +0 -301
  44. package/skills/vue/compose-with-tanstack-virtual/SKILL.md +0 -340
  45. package/skills/vue/getting-started/SKILL.md +0 -409
  46. package/skills/vue/migrate-v8-to-v9/SKILL.md +0 -375
  47. package/skills/vue/production-readiness/SKILL.md +0 -271
  48. package/skills/vue/table-state/SKILL.md +0 -403
@@ -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