@tanstack/react-table 9.0.0-alpha.9 → 9.0.0-beta.2

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 (82) hide show
  1. package/README.md +127 -0
  2. package/dist/FlexRender.cjs +61 -0
  3. package/dist/FlexRender.cjs.map +1 -0
  4. package/dist/FlexRender.d.cts +51 -0
  5. package/dist/FlexRender.d.ts +51 -0
  6. package/dist/FlexRender.js +58 -0
  7. package/dist/FlexRender.js.map +1 -0
  8. package/dist/Subscribe.cjs +13 -0
  9. package/dist/Subscribe.cjs.map +1 -0
  10. package/dist/Subscribe.d.cts +101 -0
  11. package/dist/Subscribe.d.ts +101 -0
  12. package/dist/Subscribe.js +13 -0
  13. package/dist/Subscribe.js.map +1 -0
  14. package/dist/_virtual/_rolldown/runtime.cjs +29 -0
  15. package/dist/createTableHook.cjs +313 -0
  16. package/dist/createTableHook.cjs.map +1 -0
  17. package/dist/createTableHook.d.cts +358 -0
  18. package/dist/createTableHook.d.ts +358 -0
  19. package/dist/createTableHook.js +311 -0
  20. package/dist/createTableHook.js.map +1 -0
  21. package/dist/flex-render.cjs +5 -0
  22. package/dist/flex-render.d.cts +2 -0
  23. package/dist/flex-render.d.ts +2 -0
  24. package/dist/flex-render.js +3 -0
  25. package/dist/index.cjs +18 -0
  26. package/dist/index.d.cts +6 -0
  27. package/dist/index.d.ts +6 -0
  28. package/dist/index.js +8 -0
  29. package/dist/legacy.cjs +14 -0
  30. package/dist/legacy.d.cts +2 -0
  31. package/dist/legacy.d.ts +2 -0
  32. package/dist/legacy.js +3 -0
  33. package/dist/reactivity.cjs +34 -0
  34. package/dist/reactivity.cjs.map +1 -0
  35. package/dist/reactivity.js +34 -0
  36. package/dist/reactivity.js.map +1 -0
  37. package/dist/static-functions.cjs +9 -0
  38. package/dist/static-functions.d.cts +1 -0
  39. package/dist/static-functions.d.ts +1 -0
  40. package/dist/static-functions.js +3 -0
  41. package/dist/useLegacyTable.cjs +191 -0
  42. package/dist/useLegacyTable.cjs.map +1 -0
  43. package/dist/useLegacyTable.d.cts +233 -0
  44. package/dist/useLegacyTable.d.ts +233 -0
  45. package/dist/useLegacyTable.js +181 -0
  46. package/dist/useLegacyTable.js.map +1 -0
  47. package/dist/useTable.cjs +72 -0
  48. package/dist/useTable.cjs.map +1 -0
  49. package/dist/useTable.d.cts +122 -0
  50. package/dist/useTable.d.ts +122 -0
  51. package/dist/useTable.js +72 -0
  52. package/dist/useTable.js.map +1 -0
  53. package/package.json +41 -22
  54. package/skills/react/client-to-server/SKILL.md +377 -0
  55. package/skills/react/compose-with-tanstack-form/SKILL.md +363 -0
  56. package/skills/react/compose-with-tanstack-pacer/SKILL.md +287 -0
  57. package/skills/react/compose-with-tanstack-query/SKILL.md +467 -0
  58. package/skills/react/compose-with-tanstack-store/SKILL.md +347 -0
  59. package/skills/react/compose-with-tanstack-virtual/SKILL.md +388 -0
  60. package/skills/react/compose-with-tanstack-virtual/references/column-virtualization-and-infinite-scroll.md +136 -0
  61. package/skills/react/getting-started/SKILL.md +388 -0
  62. package/skills/react/migrate-v8-to-v9/SKILL.md +488 -0
  63. package/skills/react/production-readiness/SKILL.md +341 -0
  64. package/skills/react/react-subscribe-compiler-compat/SKILL.md +269 -0
  65. package/skills/react/table-state/SKILL.md +432 -0
  66. package/src/FlexRender.tsx +136 -0
  67. package/src/Subscribe.ts +153 -0
  68. package/src/createTableHook.tsx +1121 -0
  69. package/src/flex-render.ts +1 -0
  70. package/src/index.ts +6 -0
  71. package/src/legacy.ts +3 -0
  72. package/src/reactivity.ts +41 -0
  73. package/src/static-functions.ts +1 -0
  74. package/src/useLegacyTable.ts +487 -0
  75. package/src/useTable.ts +191 -0
  76. package/dist/cjs/index.cjs +0 -77
  77. package/dist/cjs/index.cjs.map +0 -1
  78. package/dist/cjs/index.d.cts +0 -9
  79. package/dist/esm/index.d.ts +0 -9
  80. package/dist/esm/index.js +0 -55
  81. package/dist/esm/index.js.map +0 -1
  82. package/src/index.tsx +0 -92
@@ -0,0 +1,363 @@
1
+ ---
2
+ name: react/compose-with-tanstack-form
3
+ description: >
4
+ Editable cells for `@tanstack/react-table` v9 via `@tanstack/react-form`. The
5
+ table is the layout primitive; the form owns editing state. Use
6
+ `createFormHook` to register reusable field components (`TextField`,
7
+ `NumberField`, `SelectField`), then in each column's `cell` return
8
+ `<form.AppField name={`data[${row.index}].field`}>{(field) => <field.TextField />}</form.AppField>`.
9
+ Critical typing gotcha: if your row has a recursive `subRows`, use
10
+ `Omit<Row, 'subRows'>` for the form row type — TanStack Form's `DeepKeys`
11
+ recurses and hits TS2589. Subscribe to `form.state.values.data.length` (not
12
+ the whole array) for row add/remove re-renders.
13
+ type: composition
14
+ library: tanstack-table
15
+ framework: react
16
+ library_version: '9.0.0-alpha.48'
17
+ requires:
18
+ - row-selection
19
+ - column-definitions
20
+ - react/table-state
21
+ sources:
22
+ - TanStack/table:examples/react/with-tanstack-form/src/main.tsx
23
+ - TanStack/table:examples/react/with-tanstack-form/src/form.tsx
24
+ ---
25
+
26
+ This skill builds on `tanstack-table/state-management`, `tanstack-table/react/table-state`, and `tanstack-table/column-definitions`. Read those first.
27
+
28
+ ## Why this exists
29
+
30
+ TanStack Table v9 deliberately ships no built-in editing — Kevin (the maintainer) scoped it out in favor of composing with TanStack Form. The form owns row-level state, validation, dirty tracking, submit; the table is the layout/sort/filter/paginate engine. This is the v9-blessed answer to "how do I make editable cells?"
31
+
32
+ ## Setup
33
+
34
+ ```bash
35
+ pnpm add @tanstack/react-table @tanstack/react-form zod
36
+ ```
37
+
38
+ Define your field components and a form hook in a `form.tsx` module. Source: `examples/react/with-tanstack-form/src/form.tsx`.
39
+
40
+ ```tsx
41
+ import { createFormHook, createFormHookContexts } from '@tanstack/react-form'
42
+
43
+ const { fieldContext, formContext } = createFormHookContexts()
44
+
45
+ function TextField() {
46
+ /* reads field state from fieldContext */
47
+ }
48
+ function NumberField() {
49
+ /* … */
50
+ }
51
+ function SelectField() {
52
+ /* … */
53
+ }
54
+ function SubmitButton() {
55
+ /* … */
56
+ }
57
+ function FormStateIndicator() {
58
+ /* … */
59
+ }
60
+
61
+ export const { useAppForm } = createFormHook({
62
+ fieldComponents: { TextField, NumberField, SelectField },
63
+ formComponents: { SubmitButton, FormStateIndicator },
64
+ fieldContext,
65
+ formContext,
66
+ })
67
+ ```
68
+
69
+ ## Core Pattern — editable people table
70
+
71
+ ```tsx
72
+ import * as React from 'react'
73
+ import {
74
+ useTable,
75
+ tableFeatures,
76
+ columnFilteringFeature,
77
+ rowPaginationFeature,
78
+ createColumnHelper,
79
+ createFilteredRowModel,
80
+ createPaginatedRowModel,
81
+ filterFns,
82
+ } from '@tanstack/react-table'
83
+ import { useStore } from '@tanstack/react-form'
84
+ import { z } from 'zod'
85
+ import { useAppForm } from './form'
86
+ import type { Person } from './makeData'
87
+
88
+ // CRITICAL: flatten recursive subRows before handing rows to the form.
89
+ // Without Omit, TanStack Form's DeepKeys walks subRows and hits TS2589.
90
+ type FormRow = Omit<Person, 'subRows'>
91
+
92
+ const features = tableFeatures({
93
+ rowPaginationFeature,
94
+ columnFilteringFeature,
95
+ })
96
+ const columnHelper = createColumnHelper<typeof features, FormRow>()
97
+
98
+ function App() {
99
+ const initialData: FormRow[] = makeData(100)
100
+
101
+ const form = useAppForm({
102
+ defaultValues: { data: initialData },
103
+ onSubmit: ({ value }) => {
104
+ alert(`Submitted ${value.data.length} records`)
105
+ },
106
+ validators: { onChange: z.object({ data: z.array(personSchema) }) },
107
+ })
108
+
109
+ // Memo'd columns — field bindings close over `form`, so without memoization
110
+ // we'd build new column defs on every keystroke.
111
+ const columns = React.useMemo(
112
+ () =>
113
+ columnHelper.columns([
114
+ columnHelper.accessor('firstName', {
115
+ header: 'First Name',
116
+ cell: ({ row }) => (
117
+ <form.AppField
118
+ name={`data[${row.index}].firstName`}
119
+ validators={{ onChange: z.string().min(1, 'Required') }}
120
+ >
121
+ {(field) => <field.TextField />}
122
+ </form.AppField>
123
+ ),
124
+ }),
125
+ columnHelper.accessor('age', {
126
+ header: 'Age',
127
+ cell: ({ row }) => (
128
+ <form.AppField
129
+ name={`data[${row.index}].age`}
130
+ validators={{ onChange: z.number().min(0).max(150) }}
131
+ >
132
+ {(field) => <field.NumberField />}
133
+ </form.AppField>
134
+ ),
135
+ }),
136
+ columnHelper.accessor('status', {
137
+ header: 'Status',
138
+ cell: ({ row }) => (
139
+ <form.AppField name={`data[${row.index}].status`}>
140
+ {(field) => <field.SelectField />}
141
+ </form.AppField>
142
+ ),
143
+ }),
144
+ ]),
145
+ [form],
146
+ )
147
+
148
+ // Subscribe ONLY to length — triggers re-renders on add/remove without infinite loops
149
+ // (vs subscribing to data, which fires on every keystroke).
150
+ const dataLength = useStore(form.store, (state) => state.values.data.length)
151
+ void dataLength
152
+
153
+ const table = useTable({
154
+ features,
155
+ rowModels: {
156
+ filteredRowModel: createFilteredRowModel(filterFns),
157
+ paginatedRowModel: createPaginatedRowModel(),
158
+ },
159
+ columns,
160
+ data: form.state.values.data, // table reads fresh form values each render
161
+ })
162
+
163
+ const addRow = () =>
164
+ form.pushFieldValue('data', {
165
+ firstName: '',
166
+ lastName: '',
167
+ age: 0,
168
+ visits: 0,
169
+ progress: 0,
170
+ status: 'single',
171
+ })
172
+
173
+ const refreshData = () => form.reset({ data: makeData(100) })
174
+
175
+ return (
176
+ <>
177
+ <button onClick={addRow}>Add Row</button>
178
+ <button onClick={refreshData}>Refresh Data</button>
179
+ <table>
180
+ <thead>{/* … */}</thead>
181
+ <tbody>
182
+ {table.getRowModel().rows.map((row) => (
183
+ <tr key={row.id}>
184
+ {row.getAllCells().map((cell) => (
185
+ <td key={cell.id}>
186
+ <table.FlexRender cell={cell} />
187
+ </td>
188
+ ))}
189
+ </tr>
190
+ ))}
191
+ </tbody>
192
+ </table>
193
+ <form.SubmitButton />
194
+ </>
195
+ )
196
+ }
197
+ ```
198
+
199
+ Source: `examples/react/with-tanstack-form/src/main.tsx`.
200
+
201
+ ## Add / remove rows
202
+
203
+ `form.pushFieldValue('data', newRow)` adds; `form.removeFieldValue('data', index)` removes; `form.reset({ data })` replaces. The `useStore` subscription on `state.values.data.length` re-renders the holder so the table sees the new array length and renders the new row.
204
+
205
+ ## Common Mistakes
206
+
207
+ ### CRITICAL Typing rows as `Person` with recursive `subRows`
208
+
209
+ Wrong:
210
+
211
+ ```tsx
212
+ const form = useAppForm({ defaultValues: { data: makeData(100) as Person[] } })
213
+ // TanStack Form's DeepKeys walks Person.subRows recursively → TS2589
214
+ // ("Type instantiation is excessively deep and possibly infinite")
215
+ ```
216
+
217
+ Correct:
218
+
219
+ ```tsx
220
+ type FormRow = Omit<Person, 'subRows'>
221
+ const initialData: FormRow[] = makeData(100)
222
+ const form = useAppForm({ defaultValues: { data: initialData } })
223
+ const columnHelper = createColumnHelper<typeof features, FormRow>()
224
+ ```
225
+
226
+ Always strip the recursive child field from the row type you hand to the form.
227
+ Source: `examples/react/with-tanstack-form/src/main.tsx`.
228
+
229
+ ### CRITICAL Subscribing to the whole `state.values.data` array
230
+
231
+ Wrong:
232
+
233
+ ```tsx
234
+ // Every keystroke in any cell re-renders App → recreates form → re-binds every cell.
235
+ const data = useStore(form.store, (s) => s.values.data)
236
+ ```
237
+
238
+ Correct:
239
+
240
+ ```tsx
241
+ // Subscribe to length only — triggers re-renders on add/remove, ignores edits.
242
+ const dataLength = useStore(form.store, (state) => state.values.data.length)
243
+ void dataLength
244
+ // Table reads `data: form.state.values.data` directly on render.
245
+ ```
246
+
247
+ Source: `examples/react/with-tanstack-form/src/main.tsx`.
248
+
249
+ ### HIGH Forgetting `useMemo` around columns
250
+
251
+ Wrong:
252
+
253
+ ```tsx
254
+ function App() {
255
+ const form = useAppForm({
256
+ /* … */
257
+ })
258
+ const columns = columnHelper.columns([
259
+ // new column defs every render
260
+ columnHelper.accessor('firstName', {
261
+ cell: ({ row }) => (
262
+ <form.AppField name={`data[${row.index}].firstName`}>
263
+ {(field) => <field.TextField />}
264
+ </form.AppField>
265
+ ),
266
+ }),
267
+ ])
268
+ }
269
+ ```
270
+
271
+ Correct:
272
+
273
+ ```tsx
274
+ const columns = React.useMemo(
275
+ () =>
276
+ columnHelper.columns([
277
+ columnHelper.accessor('firstName', {
278
+ cell: ({ row }) => (
279
+ <form.AppField name={`data[${row.index}].firstName`}>
280
+ {(field) => <field.TextField />}
281
+ </form.AppField>
282
+ ),
283
+ }),
284
+ ]),
285
+ [form],
286
+ )
287
+ ```
288
+
289
+ Cell renderers close over `form`. Without memoization the column defs change every render, busting internal memos and remounting field components.
290
+ Source: `examples/react/with-tanstack-form/src/main.tsx`.
291
+
292
+ ### HIGH Passing the form itself in `useTable`'s `data`
293
+
294
+ Wrong:
295
+
296
+ ```tsx
297
+ const table = useTable({
298
+ features,
299
+ rowModels: {
300
+ /* … */
301
+ },
302
+ columns,
303
+ data: form, // wrong — table only needs the row array
304
+ })
305
+ ```
306
+
307
+ Correct:
308
+
309
+ ```tsx
310
+ const table = useTable({
311
+ features,
312
+ rowModels: {
313
+ /* … */
314
+ },
315
+ columns,
316
+ data: form.state.values.data,
317
+ })
318
+ ```
319
+
320
+ The table consumes the rows array. Mix the form's data into the table's `data` prop; don't try to make the table aware of the form instance.
321
+ Source: `examples/react/with-tanstack-form/src/main.tsx`.
322
+
323
+ ### MEDIUM Trying to reuse v8's `tableMeta.updateData` pattern
324
+
325
+ Wrong:
326
+
327
+ ```tsx
328
+ // v8 muscle memory: track edits in tableMeta with a per-cell useState.
329
+ const table = useReactTable({
330
+ data,
331
+ columns,
332
+ meta: {
333
+ updateData: (rowIndex, columnId, value) => {
334
+ /* manual setState dance */
335
+ },
336
+ },
337
+ })
338
+ ```
339
+
340
+ Correct:
341
+
342
+ ```tsx
343
+ // v9 idiom: TanStack Form owns the data, table renders it.
344
+ const form = useAppForm({ defaultValues: { data } })
345
+ const table = useTable({
346
+ features,
347
+ rowModels: {
348
+ /* … */
349
+ },
350
+ columns,
351
+ data: form.state.values.data,
352
+ })
353
+ ```
354
+
355
+ The v8 `tableMeta.updateData` pattern still works mechanically, but the form composition handles validation, dirty tracking, submit, and add/remove for free.
356
+ Source: maintainer guidance.
357
+
358
+ ## See Also
359
+
360
+ - `tanstack-table/react/table-state` — base table reactivity.
361
+ - `tanstack-table/react/compose-with-tanstack-pacer` — debounce column filter inputs on the same screen.
362
+ - `tanstack-table/column-definitions` — cell renderer API.
363
+ - `tanstack-table/row-selection` — row selection works alongside per-cell editing.
@@ -0,0 +1,287 @@
1
+ ---
2
+ name: react/compose-with-tanstack-pacer
3
+ description: >
4
+ Use `@tanstack/react-pacer` to debounce/throttle the high-frequency writes
5
+ that drive an interactive `@tanstack/react-table` v9 table: column filter
6
+ inputs and column resize state. Pattern: import `useDebouncedCallback`
7
+ from `@tanstack/react-pacer/debouncer`, wrap your `onChange` writer in
8
+ it, and keep local input state so typing feels instant. For column
9
+ resizing, throttle `onColumnResizingChange` so a drag doesn't push 60+
10
+ state updates per second. Pacer is the v9 replacement for the hand-rolled
11
+ `DebouncedInput` setTimeout component from v8 examples.
12
+ type: composition
13
+ library: tanstack-table
14
+ framework: react
15
+ library_version: '9.0.0-alpha.48'
16
+ requires:
17
+ - filtering
18
+ - column-layout
19
+ - react/table-state
20
+ sources:
21
+ - TanStack/table:examples/react/basic-subscribe/src/main.tsx
22
+ - TanStack/table:examples/react/with-tanstack-form/src/main.tsx
23
+ - TanStack/table:examples/react/kitchen-sink/src/main.tsx
24
+ ---
25
+
26
+ This skill builds on `tanstack-table/state-management`, `tanstack-table/react/table-state`, and the `filtering` core skill. Read those first.
27
+
28
+ ## Why this exists
29
+
30
+ A column filter input writes to `state.columnFilters` on every keystroke. Each write recomputes the filtered row model. For 100 rows that's fine; for 10k+ it's visibly janky. Pacer debounces the write so the filter only fires once after typing stops, while the input itself updates instantly via local state.
31
+
32
+ The same principle applies to drag-to-resize: a single drag can fire `onColumnResizingChange` 60+ times per second. Throttling at ~16ms (one frame) keeps the perceived smoothness without spamming the store.
33
+
34
+ ## Setup
35
+
36
+ ```bash
37
+ pnpm add @tanstack/react-pacer
38
+ ```
39
+
40
+ ```tsx
41
+ import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer'
42
+ import { useThrottledCallback } from '@tanstack/react-pacer/throttler'
43
+ ```
44
+
45
+ ## Core Pattern — `DebouncedInput` for column filters
46
+
47
+ The shape comes straight from the v9 examples — keep local input state so the input is instant, debounce the writer so the store only sees the trailing value.
48
+
49
+ ```tsx
50
+ import * as React from 'react'
51
+ import { useDebouncedCallback } from '@tanstack/react-pacer/debouncer'
52
+
53
+ function DebouncedInput({
54
+ value: initialValue,
55
+ onChange,
56
+ debounce = 300,
57
+ ...props
58
+ }: {
59
+ value: string | number
60
+ onChange: (value: string | number) => void
61
+ debounce?: number
62
+ } & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'>) {
63
+ // Local state so the input feels instant.
64
+ const [value, setValue] = React.useState(initialValue)
65
+
66
+ React.useEffect(() => {
67
+ setValue(initialValue)
68
+ }, [initialValue])
69
+
70
+ // Debounced writer — fires once after `debounce` ms of inactivity.
71
+ const debouncedOnChange = useDebouncedCallback(onChange, { wait: debounce })
72
+
73
+ return (
74
+ <input
75
+ {...props}
76
+ value={value}
77
+ onChange={(e) => {
78
+ setValue(e.target.value) // instant UI update
79
+ debouncedOnChange(e.target.value) // debounced store write
80
+ }}
81
+ />
82
+ )
83
+ }
84
+
85
+ // Usage in a column filter:
86
+ ;<DebouncedInput
87
+ value={(column.getFilterValue() ?? '') as string}
88
+ onChange={(v) => column.setFilterValue(v)}
89
+ placeholder="Search..."
90
+ />
91
+ ```
92
+
93
+ Source: `examples/react/basic-subscribe/src/main.tsx`; `examples/react/with-tanstack-form/src/main.tsx`.
94
+
95
+ ## Debounce vs throttle — choose by intent
96
+
97
+ | Pattern | Use case | Typical wait |
98
+ | ------------ | ------------------------------------------------------------------------ | ---------------- |
99
+ | **Debounce** | "Wait until they stop typing, then commit" — filter inputs, search boxes | 250–500ms |
100
+ | **Throttle** | "Fire at most every N ms" — drag-to-resize, scroll-triggered fetches | 16ms (one frame) |
101
+
102
+ ```tsx
103
+ // Throttled column resize
104
+ const throttledResize = useThrottledCallback(
105
+ (next: ColumnResizingState) => columnResizingAtom.set(next),
106
+ { wait: 16 },
107
+ )
108
+ ```
109
+
110
+ ## Global filter with debounce + instant input
111
+
112
+ The exact pattern used in `examples/react/basic-subscribe/src/main.tsx`:
113
+
114
+ ```tsx
115
+ <table.Subscribe source={table.atoms.globalFilter}>
116
+ {(globalFilter) => (
117
+ <DebouncedInput
118
+ value={globalFilter ?? ''}
119
+ onChange={(value) => table.setGlobalFilter(value)}
120
+ placeholder="Search all columns..."
121
+ />
122
+ )}
123
+ </table.Subscribe>
124
+ ```
125
+
126
+ The `<table.Subscribe>` wrapper keeps the input controlled by the table's atom (so external resets propagate), while `DebouncedInput`'s local state keeps the visual update instant.
127
+
128
+ ## Common Mistakes
129
+
130
+ ### CRITICAL Writing filter values directly on every keystroke
131
+
132
+ Wrong:
133
+
134
+ ```tsx
135
+ <input onChange={(e) => column.setFilterValue(e.target.value)} />
136
+ // On a 10k-row table, this recomputes the filtered row model per character — janky.
137
+ ```
138
+
139
+ Correct:
140
+
141
+ ```tsx
142
+ <DebouncedInput
143
+ value={(column.getFilterValue() ?? '') as string}
144
+ onChange={(v) => column.setFilterValue(v)}
145
+ />
146
+ ```
147
+
148
+ Debounce the store write; keep the input instant via local state.
149
+ Source: `examples/react/basic-subscribe/src/main.tsx`.
150
+
151
+ ### HIGH Rolling your own `setTimeout` debounce
152
+
153
+ Wrong:
154
+
155
+ ```tsx
156
+ function DebouncedInput({ value, onChange, debounce = 300 }) {
157
+ const [v, setV] = React.useState(value)
158
+ React.useEffect(() => {
159
+ const t = setTimeout(() => onChange(v), debounce)
160
+ return () => clearTimeout(t)
161
+ }, [v])
162
+ // Works, but reinvents what useDebouncedCallback already provides with proper
163
+ // ref stability, cleanup, and trailing-edge semantics.
164
+ }
165
+ ```
166
+
167
+ Correct:
168
+
169
+ ```tsx
170
+ function DebouncedInput({
171
+ value: initialValue,
172
+ onChange,
173
+ debounce = 300,
174
+ ...props
175
+ }) {
176
+ const [value, setValue] = React.useState(initialValue)
177
+ React.useEffect(() => {
178
+ setValue(initialValue)
179
+ }, [initialValue])
180
+ const debouncedOnChange = useDebouncedCallback(onChange, { wait: debounce })
181
+ return (
182
+ <input
183
+ {...props}
184
+ value={value}
185
+ onChange={(e) => {
186
+ setValue(e.target.value)
187
+ debouncedOnChange(e.target.value)
188
+ }}
189
+ />
190
+ )
191
+ }
192
+ ```
193
+
194
+ v8 examples shipped the hand-rolled version. v9 explicitly delegates to Pacer.
195
+ Source: `examples/react/basic-subscribe/src/main.tsx`.
196
+
197
+ ### HIGH Debouncing the local input state instead of (or in addition to) the writer
198
+
199
+ Wrong:
200
+
201
+ ```tsx
202
+ const debouncedSetLocal = useDebouncedCallback(setValue, { wait: 300 })
203
+ <input value={value} onChange={(e) => debouncedSetLocal(e.target.value)} />
204
+ // User sees stale characters in the input box.
205
+ ```
206
+
207
+ Correct:
208
+
209
+ ```tsx
210
+ const debouncedOnChange = useDebouncedCallback(onChange, { wait: 300 })
211
+ <input
212
+ value={value}
213
+ onChange={(e) => {
214
+ setValue(e.target.value) // instant local update
215
+ debouncedOnChange(e.target.value) // debounced store write only
216
+ }}
217
+ />
218
+ ```
219
+
220
+ Local state should always be instant. Only the expensive store write should debounce.
221
+ Source: `examples/react/basic-subscribe/src/main.tsx`.
222
+
223
+ ### HIGH Throttling column resize at 250ms
224
+
225
+ Wrong:
226
+
227
+ ```tsx
228
+ const throttledResize = useThrottledCallback(setResize, { wait: 250 })
229
+ // 4 fps drag → visibly laggy.
230
+ ```
231
+
232
+ Correct:
233
+
234
+ ```tsx
235
+ const throttledResize = useThrottledCallback(setResize, { wait: 16 })
236
+ // ~60 fps, smooth.
237
+ ```
238
+
239
+ Use roughly one frame (16ms). 250ms is fine for filter writes; far too long for drag interactions.
240
+ Source: maintainer guidance.
241
+
242
+ ### MEDIUM Wrapping `column.setFilterValue` directly without local input state
243
+
244
+ Wrong:
245
+
246
+ ```tsx
247
+ const debouncedSet = useDebouncedCallback(column.setFilterValue, { wait: 300 })
248
+ <input onChange={(e) => debouncedSet(e.target.value)} />
249
+ // Input becomes uncontrolled-feeling because the render goes through the slow path.
250
+ ```
251
+
252
+ Correct:
253
+
254
+ ```tsx
255
+ <DebouncedInput
256
+ value={(column.getFilterValue() ?? '') as string}
257
+ onChange={(v) => column.setFilterValue(v)}
258
+ />
259
+ ```
260
+
261
+ The `DebouncedInput` pattern combines local state (instant) with debounced commit (cheap). Don't skip the local state.
262
+ Source: `examples/react/basic-subscribe/src/main.tsx`.
263
+
264
+ ### MEDIUM Using `wait: 0` and expecting debouncing
265
+
266
+ Wrong:
267
+
268
+ ```tsx
269
+ const debouncedOnChange = useDebouncedCallback(onChange, { wait: 0 })
270
+ // Effectively no debounce — fires synchronously.
271
+ ```
272
+
273
+ Correct:
274
+
275
+ ```tsx
276
+ const debouncedOnChange = useDebouncedCallback(onChange, { wait: 300 })
277
+ ```
278
+
279
+ `wait` is meaningful. 250–500ms is the typical sweet spot for filter inputs; 16ms is the typical sweet spot for resize/scroll throttling.
280
+ Source: maintainer guidance.
281
+
282
+ ## See Also
283
+
284
+ - `tanstack-table/react/table-state` — Subscribe boundaries for the debounced writers to feed into.
285
+ - `tanstack-table/filtering` — column filter feature API surface.
286
+ - `tanstack-table/column-layout` — column resize feature.
287
+ - `tanstack-table/react/compose-with-tanstack-query` — debounce filter input that feeds a server-side query.