@tanstack/preact-table 9.0.0-alpha.46 → 9.0.0-alpha.48

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 CHANGED
@@ -49,6 +49,16 @@ A headless table library for building powerful datagrids with full control over
49
49
 
50
50
  ### <a href="https://tanstack.com/table">Read the Docs →</a>
51
51
 
52
+ ## Using an AI Coding Agent?
53
+
54
+ TanStack Table ships [TanStack Intent](https://github.com/TanStack/intent) skills inside each adapter package. After installing the library, run:
55
+
56
+ ```sh
57
+ npx @tanstack/intent@latest install
58
+ ```
59
+
60
+ to add skill-loading guidance for your agent (Claude Code, Cursor, Copilot, etc.). The same CLI also exposes `intent list` to browse available skills and `intent load <skill>` to print one for inspection. Skills version with the library — your agent gets guidance that matches the version of `@tanstack/<framework>-table` you installed. Only available for v9 and above.
61
+
52
62
  ## Get Involved
53
63
 
54
64
  - We welcome issues and pull requests!
@@ -10,6 +10,7 @@ let _tanstack_preact_store = require("@tanstack/preact-store");
10
10
  function preactReactivity() {
11
11
  return {
12
12
  createOptionsStore: false,
13
+ schedule: (fn) => queueMicrotask(() => fn()),
13
14
  batch: _tanstack_preact_store.batch,
14
15
  untrack: (fn) => fn(),
15
16
  createReadonlyAtom: (fn, options) => {
@@ -1 +1 @@
1
- {"version":3,"file":"reactivity.cjs","names":[],"sources":["../src/reactivity.ts"],"sourcesContent":["import { batch, createAtom } from '@tanstack/preact-store'\nimport type {\n TableAtomOptions,\n TableReactivityBindings,\n} from '@tanstack/table-core/reactivity'\n\n/**\n * Creates the table-core reactivity bindings used by the Preact adapter.\n *\n * Preact stores table state in TanStack Store atoms and leaves options as plain\n * resolved data because `useTable` synchronizes options during render.\n */\nexport function preactReactivity(): TableReactivityBindings {\n return {\n createOptionsStore: false,\n batch,\n untrack: (fn) => fn(),\n createReadonlyAtom: <T>(fn: () => T, options?: TableAtomOptions<T>) => {\n return createAtom(() => fn(), {\n compare: options?.compare,\n })\n },\n createWritableAtom: <T>(value: T, options?: TableAtomOptions<T>) => {\n return createAtom(value, {\n compare: options?.compare,\n })\n },\n }\n}\n\n// // TOTO - re-explore preact signals for reactivity\n// import { batch, computed, signal, untracked } from '@preact/signals'\n// import type {\n// TableAtomOptions,\n// TableReactivityBindings,\n// } from '@tanstack/table-core/reactivity'\n// import type { Atom, Observer, ReadonlyAtom } from '@tanstack/preact-store'\n\n// function observerToCallback<T>(\n// observerOrNext: Observer<T> | ((value: T) => void),\n// ): (value: T) => void {\n// return typeof observerOrNext === 'function'\n// ? observerOrNext\n// : (value) => observerOrNext.next?.(value)\n// }\n\n// function signalToReadonlyAtom<T>(source: {\n// value: T\n// subscribe: (observer: (value: T) => void) => () => void\n// }): ReadonlyAtom<T> {\n// return Object.assign(source, {\n// get: () => source.value,\n// subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {\n// const unsubscribe = source.subscribe(observerToCallback(observerOrNext))\n// return { unsubscribe }\n// }) as ReadonlyAtom<T>['subscribe'],\n// })\n// }\n\n// function signalToWritableAtom<T>(source: {\n// value: T\n// subscribe: (observer: (value: T) => void) => () => void\n// }): Atom<T> {\n// return Object.assign(source, {\n// set: (updater: T | ((prevVal: T) => T)) => {\n// source.value =\n// typeof updater === 'function'\n// ? (updater as (prevVal: T) => T)(source.value)\n// : updater\n// },\n// get: () => source.value,\n// subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {\n// const unsubscribe = source.subscribe(observerToCallback(observerOrNext))\n// return { unsubscribe }\n// }) as Atom<T>['subscribe'],\n// })\n// }\n\n// export function preactReactivity(): TableReactivityBindings {\n// return {\n// createReadonlyAtom: <T>(fn: () => T, _options?: TableAtomOptions<T>) => {\n// return signalToReadonlyAtom(computed(fn))\n// },\n// createWritableAtom: <T>(\n// value: T,\n// _options?: TableAtomOptions<T>,\n// ): Atom<T> => {\n// return signalToWritableAtom(signal(value))\n// },\n// untrack: untracked,\n// batch: batch,\n// }\n// }\n"],"mappings":";;;;;;;;;AAYA,SAAgB,mBAA4C;CAC1D,OAAO;EACL,oBAAoB;EACpB;EACA,UAAU,OAAO,IAAI;EACrB,qBAAwB,IAAa,YAAkC;GACrE,oDAAwB,IAAI,EAAE,EAC5B,2DAAS,QAAS,SACnB,CAAC;;EAEJ,qBAAwB,OAAU,YAAkC;GAClE,8CAAkB,OAAO,EACvB,2DAAS,QAAS,SACnB,CAAC;;EAEL"}
1
+ {"version":3,"file":"reactivity.cjs","names":[],"sources":["../src/reactivity.ts"],"sourcesContent":["import { batch, createAtom } from '@tanstack/preact-store'\nimport type {\n TableAtomOptions,\n TableReactivityBindings,\n} from '@tanstack/table-core/reactivity'\n\n/**\n * Creates the table-core reactivity bindings used by the Preact adapter.\n *\n * Preact stores table state in TanStack Store atoms and leaves options as plain\n * resolved data because `useTable` synchronizes options during render.\n */\nexport function preactReactivity(): TableReactivityBindings {\n return {\n createOptionsStore: false,\n schedule: (fn) => queueMicrotask(() => fn()),\n batch,\n untrack: (fn) => fn(),\n createReadonlyAtom: <T>(fn: () => T, options?: TableAtomOptions<T>) => {\n return createAtom(() => fn(), {\n compare: options?.compare,\n })\n },\n createWritableAtom: <T>(value: T, options?: TableAtomOptions<T>) => {\n return createAtom(value, {\n compare: options?.compare,\n })\n },\n }\n}\n\n// // TOTO - re-explore preact signals for reactivity\n// import { batch, computed, signal, untracked } from '@preact/signals'\n// import type {\n// TableAtomOptions,\n// TableReactivityBindings,\n// } from '@tanstack/table-core/reactivity'\n// import type { Atom, Observer, ReadonlyAtom } from '@tanstack/preact-store'\n\n// function observerToCallback<T>(\n// observerOrNext: Observer<T> | ((value: T) => void),\n// ): (value: T) => void {\n// return typeof observerOrNext === 'function'\n// ? observerOrNext\n// : (value) => observerOrNext.next?.(value)\n// }\n\n// function signalToReadonlyAtom<T>(source: {\n// value: T\n// subscribe: (observer: (value: T) => void) => () => void\n// }): ReadonlyAtom<T> {\n// return Object.assign(source, {\n// get: () => source.value,\n// subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {\n// const unsubscribe = source.subscribe(observerToCallback(observerOrNext))\n// return { unsubscribe }\n// }) as ReadonlyAtom<T>['subscribe'],\n// })\n// }\n\n// function signalToWritableAtom<T>(source: {\n// value: T\n// subscribe: (observer: (value: T) => void) => () => void\n// }): Atom<T> {\n// return Object.assign(source, {\n// set: (updater: T | ((prevVal: T) => T)) => {\n// source.value =\n// typeof updater === 'function'\n// ? (updater as (prevVal: T) => T)(source.value)\n// : updater\n// },\n// get: () => source.value,\n// subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {\n// const unsubscribe = source.subscribe(observerToCallback(observerOrNext))\n// return { unsubscribe }\n// }) as Atom<T>['subscribe'],\n// })\n// }\n\n// export function preactReactivity(): TableReactivityBindings {\n// return {\n// createReadonlyAtom: <T>(fn: () => T, _options?: TableAtomOptions<T>) => {\n// return signalToReadonlyAtom(computed(fn))\n// },\n// createWritableAtom: <T>(\n// value: T,\n// _options?: TableAtomOptions<T>,\n// ): Atom<T> => {\n// return signalToWritableAtom(signal(value))\n// },\n// untrack: untracked,\n// batch: batch,\n// }\n// }\n"],"mappings":";;;;;;;;;AAYA,SAAgB,mBAA4C;CAC1D,OAAO;EACL,oBAAoB;EACpB,WAAW,OAAO,qBAAqB,IAAI,CAAC;EAC5C;EACA,UAAU,OAAO,IAAI;EACrB,qBAAwB,IAAa,YAAkC;GACrE,oDAAwB,IAAI,EAAE,EAC5B,2DAAS,QAAS,SACnB,CAAC;;EAEJ,qBAAwB,OAAU,YAAkC;GAClE,8CAAkB,OAAO,EACvB,2DAAS,QAAS,SACnB,CAAC;;EAEL"}
@@ -10,6 +10,7 @@ import { batch, createAtom } from "@tanstack/preact-store";
10
10
  function preactReactivity() {
11
11
  return {
12
12
  createOptionsStore: false,
13
+ schedule: (fn) => queueMicrotask(() => fn()),
13
14
  batch,
14
15
  untrack: (fn) => fn(),
15
16
  createReadonlyAtom: (fn, options) => {
@@ -1 +1 @@
1
- {"version":3,"file":"reactivity.js","names":[],"sources":["../src/reactivity.ts"],"sourcesContent":["import { batch, createAtom } from '@tanstack/preact-store'\nimport type {\n TableAtomOptions,\n TableReactivityBindings,\n} from '@tanstack/table-core/reactivity'\n\n/**\n * Creates the table-core reactivity bindings used by the Preact adapter.\n *\n * Preact stores table state in TanStack Store atoms and leaves options as plain\n * resolved data because `useTable` synchronizes options during render.\n */\nexport function preactReactivity(): TableReactivityBindings {\n return {\n createOptionsStore: false,\n batch,\n untrack: (fn) => fn(),\n createReadonlyAtom: <T>(fn: () => T, options?: TableAtomOptions<T>) => {\n return createAtom(() => fn(), {\n compare: options?.compare,\n })\n },\n createWritableAtom: <T>(value: T, options?: TableAtomOptions<T>) => {\n return createAtom(value, {\n compare: options?.compare,\n })\n },\n }\n}\n\n// // TOTO - re-explore preact signals for reactivity\n// import { batch, computed, signal, untracked } from '@preact/signals'\n// import type {\n// TableAtomOptions,\n// TableReactivityBindings,\n// } from '@tanstack/table-core/reactivity'\n// import type { Atom, Observer, ReadonlyAtom } from '@tanstack/preact-store'\n\n// function observerToCallback<T>(\n// observerOrNext: Observer<T> | ((value: T) => void),\n// ): (value: T) => void {\n// return typeof observerOrNext === 'function'\n// ? observerOrNext\n// : (value) => observerOrNext.next?.(value)\n// }\n\n// function signalToReadonlyAtom<T>(source: {\n// value: T\n// subscribe: (observer: (value: T) => void) => () => void\n// }): ReadonlyAtom<T> {\n// return Object.assign(source, {\n// get: () => source.value,\n// subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {\n// const unsubscribe = source.subscribe(observerToCallback(observerOrNext))\n// return { unsubscribe }\n// }) as ReadonlyAtom<T>['subscribe'],\n// })\n// }\n\n// function signalToWritableAtom<T>(source: {\n// value: T\n// subscribe: (observer: (value: T) => void) => () => void\n// }): Atom<T> {\n// return Object.assign(source, {\n// set: (updater: T | ((prevVal: T) => T)) => {\n// source.value =\n// typeof updater === 'function'\n// ? (updater as (prevVal: T) => T)(source.value)\n// : updater\n// },\n// get: () => source.value,\n// subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {\n// const unsubscribe = source.subscribe(observerToCallback(observerOrNext))\n// return { unsubscribe }\n// }) as Atom<T>['subscribe'],\n// })\n// }\n\n// export function preactReactivity(): TableReactivityBindings {\n// return {\n// createReadonlyAtom: <T>(fn: () => T, _options?: TableAtomOptions<T>) => {\n// return signalToReadonlyAtom(computed(fn))\n// },\n// createWritableAtom: <T>(\n// value: T,\n// _options?: TableAtomOptions<T>,\n// ): Atom<T> => {\n// return signalToWritableAtom(signal(value))\n// },\n// untrack: untracked,\n// batch: batch,\n// }\n// }\n"],"mappings":";;;;;;;;;AAYA,SAAgB,mBAA4C;CAC1D,OAAO;EACL,oBAAoB;EACpB;EACA,UAAU,OAAO,IAAI;EACrB,qBAAwB,IAAa,YAAkC;GACrE,OAAO,iBAAiB,IAAI,EAAE,EAC5B,2DAAS,QAAS,SACnB,CAAC;;EAEJ,qBAAwB,OAAU,YAAkC;GAClE,OAAO,WAAW,OAAO,EACvB,2DAAS,QAAS,SACnB,CAAC;;EAEL"}
1
+ {"version":3,"file":"reactivity.js","names":[],"sources":["../src/reactivity.ts"],"sourcesContent":["import { batch, createAtom } from '@tanstack/preact-store'\nimport type {\n TableAtomOptions,\n TableReactivityBindings,\n} from '@tanstack/table-core/reactivity'\n\n/**\n * Creates the table-core reactivity bindings used by the Preact adapter.\n *\n * Preact stores table state in TanStack Store atoms and leaves options as plain\n * resolved data because `useTable` synchronizes options during render.\n */\nexport function preactReactivity(): TableReactivityBindings {\n return {\n createOptionsStore: false,\n schedule: (fn) => queueMicrotask(() => fn()),\n batch,\n untrack: (fn) => fn(),\n createReadonlyAtom: <T>(fn: () => T, options?: TableAtomOptions<T>) => {\n return createAtom(() => fn(), {\n compare: options?.compare,\n })\n },\n createWritableAtom: <T>(value: T, options?: TableAtomOptions<T>) => {\n return createAtom(value, {\n compare: options?.compare,\n })\n },\n }\n}\n\n// // TOTO - re-explore preact signals for reactivity\n// import { batch, computed, signal, untracked } from '@preact/signals'\n// import type {\n// TableAtomOptions,\n// TableReactivityBindings,\n// } from '@tanstack/table-core/reactivity'\n// import type { Atom, Observer, ReadonlyAtom } from '@tanstack/preact-store'\n\n// function observerToCallback<T>(\n// observerOrNext: Observer<T> | ((value: T) => void),\n// ): (value: T) => void {\n// return typeof observerOrNext === 'function'\n// ? observerOrNext\n// : (value) => observerOrNext.next?.(value)\n// }\n\n// function signalToReadonlyAtom<T>(source: {\n// value: T\n// subscribe: (observer: (value: T) => void) => () => void\n// }): ReadonlyAtom<T> {\n// return Object.assign(source, {\n// get: () => source.value,\n// subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {\n// const unsubscribe = source.subscribe(observerToCallback(observerOrNext))\n// return { unsubscribe }\n// }) as ReadonlyAtom<T>['subscribe'],\n// })\n// }\n\n// function signalToWritableAtom<T>(source: {\n// value: T\n// subscribe: (observer: (value: T) => void) => () => void\n// }): Atom<T> {\n// return Object.assign(source, {\n// set: (updater: T | ((prevVal: T) => T)) => {\n// source.value =\n// typeof updater === 'function'\n// ? (updater as (prevVal: T) => T)(source.value)\n// : updater\n// },\n// get: () => source.value,\n// subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {\n// const unsubscribe = source.subscribe(observerToCallback(observerOrNext))\n// return { unsubscribe }\n// }) as Atom<T>['subscribe'],\n// })\n// }\n\n// export function preactReactivity(): TableReactivityBindings {\n// return {\n// createReadonlyAtom: <T>(fn: () => T, _options?: TableAtomOptions<T>) => {\n// return signalToReadonlyAtom(computed(fn))\n// },\n// createWritableAtom: <T>(\n// value: T,\n// _options?: TableAtomOptions<T>,\n// ): Atom<T> => {\n// return signalToWritableAtom(signal(value))\n// },\n// untrack: untracked,\n// batch: batch,\n// }\n// }\n"],"mappings":";;;;;;;;;AAYA,SAAgB,mBAA4C;CAC1D,OAAO;EACL,oBAAoB;EACpB,WAAW,OAAO,qBAAqB,IAAI,CAAC;EAC5C;EACA,UAAU,OAAO,IAAI;EACrB,qBAAwB,IAAa,YAAkC;GACrE,OAAO,iBAAiB,IAAI,EAAE,EAC5B,2DAAS,QAAS,SACnB,CAAC;;EAEJ,qBAAwB,OAAU,YAAkC;GAClE,OAAO,WAAW,OAAO,EACvB,2DAAS,QAAS,SACnB,CAAC;;EAEL"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/preact-table",
3
- "version": "9.0.0-alpha.46",
3
+ "version": "9.0.0-alpha.48",
4
4
  "description": "Headless UI for building powerful tables & datagrids for Preact.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -18,7 +18,8 @@
18
18
  "preact",
19
19
  "table",
20
20
  "preact-table",
21
- "datagrid"
21
+ "datagrid",
22
+ "tanstack-intent"
22
23
  ],
23
24
  "type": "module",
24
25
  "types": "./dist/index.d.cts",
@@ -45,11 +46,12 @@
45
46
  },
46
47
  "files": [
47
48
  "dist",
48
- "src"
49
+ "src",
50
+ "skills"
49
51
  ],
50
52
  "dependencies": {
51
53
  "@tanstack/preact-store": "^0.13.1",
52
- "@tanstack/table-core": "9.0.0-alpha.46"
54
+ "@tanstack/table-core": "9.0.0-alpha.48"
53
55
  },
54
56
  "devDependencies": {
55
57
  "@preact/preset-vite": "^2.10.5",
@@ -0,0 +1,294 @@
1
+ ---
2
+ name: preact/client-to-server
3
+ description: >
4
+ Convert a client-side `@tanstack/preact-table` to server-side (a.k.a. manual)
5
+ modes. Pass server-paginated data, set `manualSorting` / `manualFiltering` /
6
+ `manualPagination` / `manualGrouping` / `manualExpanding` for whatever the
7
+ server owns, supply `rowCount`, key external atoms for pagination/sorting/
8
+ filters and trigger a refetch when they change. Routing keywords: server-side
9
+ pagination, manual pagination, manualSorting, manualFiltering, rowCount,
10
+ remote data preact.
11
+ type: lifecycle
12
+ library: tanstack-table
13
+ framework: preact
14
+ library_version: '9.0.0-alpha.47'
15
+ requires:
16
+ - state-management
17
+ - pagination
18
+ - filtering
19
+ - sorting
20
+ - preact/table-state
21
+ sources:
22
+ - TanStack/table:examples/preact/basic-external-atoms/src/main.tsx
23
+ - TanStack/table:examples/preact/with-tanstack-query/src/main.tsx
24
+ - TanStack/table:examples/preact/with-tanstack-query/src/fetchData.ts
25
+ - TanStack/table:docs/framework/preact/guide/table-state.md
26
+ ---
27
+
28
+ Client-side tables run sort/filter/paginate through registered row-model factories. Server-side tables let the server own those stages; the table just renders what the server returned and emits state changes that the app uses to refetch. Same `_features`, same APIs — different ownership.
29
+
30
+ ## The Manual Flags
31
+
32
+ Set the matching flag(s) to `true` to tell the table that the server (not the registered row-model factory) is doing that stage:
33
+
34
+ | Flag | Owned by server |
35
+ | ------------------ | ----------------------- |
36
+ | `manualPagination` | page slicing |
37
+ | `manualSorting` | row ordering |
38
+ | `manualFiltering` | column + global filters |
39
+ | `manualGrouping` | group-by rows |
40
+ | `manualExpanding` | row expansion |
41
+
42
+ The matching `*Feature` should still be in `_features` so its state slice exists and its APIs work — you are only telling the row-model pipeline to skip the transform.
43
+
44
+ For pagination, supply `rowCount` so `table.getPageCount()` is correct. Optional but usually required for a UI.
45
+
46
+ Source: `examples/preact/with-tanstack-query/src/main.tsx`.
47
+
48
+ ## Standard Pattern
49
+
50
+ Own the slices that drive the server request with external atoms. Subscribe to them with `useSelector` so the request key is reactive. Pass them through `options.atoms`. Trigger the refetch from the same atoms.
51
+
52
+ ```tsx
53
+ import { useMemo } from 'preact/hooks'
54
+ import { useCreateAtom, useSelector } from '@tanstack/preact-store'
55
+ import {
56
+ rowPaginationFeature,
57
+ tableFeatures,
58
+ useTable,
59
+ type PaginationState,
60
+ } from '@tanstack/preact-table'
61
+
62
+ const _features = tableFeatures({ rowPaginationFeature })
63
+
64
+ function App() {
65
+ const paginationAtom = useCreateAtom<PaginationState>({
66
+ pageIndex: 0,
67
+ pageSize: 10,
68
+ })
69
+ const pagination = useSelector(paginationAtom)
70
+
71
+ // Any data fetcher works — fetch / SWR / preact-query / a Suspense source.
72
+ const { data: rowsPayload } = useSomeServerFetcher({
73
+ queryKey: ['rows', pagination],
74
+ queryFn: () => fetchRows(pagination),
75
+ })
76
+
77
+ const defaultData = useMemo(() => [], [])
78
+
79
+ const table = useTable(
80
+ {
81
+ _features,
82
+ _rowModels: {}, // no client-side pagination factory
83
+ columns,
84
+ data: rowsPayload?.rows ?? defaultData,
85
+ rowCount: rowsPayload?.rowCount, // makes getPageCount() correct
86
+ atoms: { pagination: paginationAtom },
87
+ manualPagination: true, // server owns the slicing
88
+ },
89
+ (state) => state,
90
+ )
91
+ // ...
92
+ }
93
+ ```
94
+
95
+ Source: `examples/preact/with-tanstack-query/src/main.tsx` (lines 56–86).
96
+
97
+ ## All Three Slices Server-Owned
98
+
99
+ Same shape, more atoms. Compose `pagination + sorting + columnFilters + globalFilter` into the request key.
100
+
101
+ ```tsx
102
+ const _features = tableFeatures({
103
+ rowPaginationFeature,
104
+ rowSortingFeature,
105
+ columnFilteringFeature,
106
+ globalFilteringFeature,
107
+ })
108
+
109
+ const paginationAtom = useCreateAtom<PaginationState>({
110
+ pageIndex: 0,
111
+ pageSize: 10,
112
+ })
113
+ const sortingAtom = useCreateAtom<SortingState>([])
114
+ const columnFiltersAtom = useCreateAtom<ColumnFiltersState>([])
115
+ const globalFilterAtom = useCreateAtom<string>('')
116
+
117
+ const pagination = useSelector(paginationAtom)
118
+ const sorting = useSelector(sortingAtom)
119
+ const columnFilters = useSelector(columnFiltersAtom)
120
+ const globalFilter = useSelector(globalFilterAtom)
121
+
122
+ const { data } = useSomeServerFetcher({
123
+ queryKey: ['rows', pagination, sorting, columnFilters, globalFilter],
124
+ queryFn: () =>
125
+ fetchRows({ pagination, sorting, columnFilters, globalFilter }),
126
+ })
127
+
128
+ const table = useTable({
129
+ _features,
130
+ _rowModels: {}, // server owns every stage
131
+ columns,
132
+ data: data?.rows ?? EMPTY,
133
+ rowCount: data?.rowCount,
134
+ atoms: {
135
+ pagination: paginationAtom,
136
+ sorting: sortingAtom,
137
+ columnFilters: columnFiltersAtom,
138
+ globalFilter: globalFilterAtom,
139
+ },
140
+ manualPagination: true,
141
+ manualSorting: true,
142
+ manualFiltering: true,
143
+ })
144
+ ```
145
+
146
+ When `manualFiltering: true`, both `columnFilters` and the global filter are treated as server-owned.
147
+
148
+ ## Common Mistakes
149
+
150
+ ### CRITICAL Setting `manualPagination` without `rowCount`
151
+
152
+ Wrong:
153
+
154
+ ```tsx
155
+ useTable({
156
+ _features,
157
+ _rowModels: {},
158
+ columns,
159
+ data: response?.rows ?? [],
160
+ atoms: { pagination: paginationAtom },
161
+ manualPagination: true,
162
+ // no rowCount
163
+ })
164
+ table.getPageCount() // Infinity / wrong
165
+ ```
166
+
167
+ Correct:
168
+
169
+ ```tsx
170
+ useTable({
171
+ _features,
172
+ _rowModels: {},
173
+ columns,
174
+ data: response?.rows ?? [],
175
+ rowCount: response?.rowCount,
176
+ atoms: { pagination: paginationAtom },
177
+ manualPagination: true,
178
+ })
179
+ ```
180
+
181
+ Without `rowCount` the table cannot know how many pages exist.
182
+ Source: `examples/preact/with-tanstack-query/src/main.tsx`.
183
+
184
+ ### HIGH Keeping the client-side row model when going manual
185
+
186
+ Wrong:
187
+
188
+ ```tsx
189
+ useTable({
190
+ _features,
191
+ _rowModels: { paginatedRowModel: createPaginatedRowModel() }, // still runs
192
+ data: server.rows,
193
+ manualPagination: true,
194
+ })
195
+ ```
196
+
197
+ Correct:
198
+
199
+ ```tsx
200
+ useTable({
201
+ _features,
202
+ _rowModels: {}, // server owns pagination
203
+ data: server.rows,
204
+ rowCount: server.rowCount,
205
+ manualPagination: true,
206
+ })
207
+ ```
208
+
209
+ With `manualPagination`, the paginated row model has nothing useful to do — drop it. Same for `sortedRowModel` under `manualSorting`, `filteredRowModel` under `manualFiltering`.
210
+ Source: `examples/preact/with-tanstack-query/src/main.tsx`.
211
+
212
+ ### HIGH Forgetting to key the request on the slices the server owns
213
+
214
+ Wrong:
215
+
216
+ ```tsx
217
+ const { data } = useQuery({
218
+ queryKey: ['rows'], // never changes
219
+ queryFn: () => fetchRows(pagination),
220
+ })
221
+ ```
222
+
223
+ Correct:
224
+
225
+ ```tsx
226
+ const { data } = useQuery({
227
+ queryKey: ['rows', pagination, sorting, columnFilters, globalFilter],
228
+ queryFn: () =>
229
+ fetchRows({ pagination, sorting, columnFilters, globalFilter }),
230
+ })
231
+ ```
232
+
233
+ The request must vary by the slice values; otherwise the fetcher cache returns stale data when the user sorts or pages.
234
+ Source: `examples/preact/with-tanstack-query/src/main.tsx`.
235
+
236
+ ### HIGH Page flashes empty between fetches
237
+
238
+ Wrong: the request resolves to `undefined` while loading, so the table shows zero rows between pages.
239
+
240
+ Correct: pass a stable `defaultData` and (with @tanstack/preact-query) `placeholderData: keepPreviousData`. The table re-uses the last page's rows during the fetch.
241
+
242
+ ```tsx
243
+ import { keepPreviousData } from '@tanstack/preact-query'
244
+
245
+ const defaultData = useMemo(() => [], [])
246
+
247
+ const { data } = useQuery({
248
+ queryKey: ['rows', pagination],
249
+ queryFn: () => fetchRows(pagination),
250
+ placeholderData: keepPreviousData,
251
+ })
252
+
253
+ const table = useTable({
254
+ _features,
255
+ _rowModels: {},
256
+ columns,
257
+ data: data?.rows ?? defaultData,
258
+ rowCount: data?.rowCount,
259
+ atoms: { pagination: paginationAtom },
260
+ manualPagination: true,
261
+ })
262
+ ```
263
+
264
+ Source: `examples/preact/with-tanstack-query/src/main.tsx`.
265
+
266
+ ### MEDIUM Removing the matching feature from `_features`
267
+
268
+ Wrong:
269
+
270
+ ```tsx
271
+ const _features = tableFeatures({}) // dropped rowPaginationFeature
272
+ useTable({
273
+ _features,
274
+ _rowModels: {},
275
+ data: server.rows,
276
+ rowCount: server.rowCount,
277
+ manualPagination: true,
278
+ })
279
+ table.setPageIndex(0) // type error / no-op
280
+ ```
281
+
282
+ Correct: keep the feature registered. `manualPagination: true` only tells the row-model pipeline to skip slicing — you still want the pagination state slice and `setPageIndex` / `nextPage` APIs.
283
+
284
+ ```tsx
285
+ const _features = tableFeatures({ rowPaginationFeature })
286
+ ```
287
+
288
+ Source: `docs/guide/features.md`.
289
+
290
+ ## See Also
291
+
292
+ - `tanstack-table/preact/compose-with-tanstack-query` — full @tanstack/preact-query recipe with keepPreviousData and refetch ergonomics.
293
+ - `tanstack-table/preact/table-state` — atoms vs state, table.Subscribe.
294
+ - `tanstack-table/pagination`, `tanstack-table/filtering`, `tanstack-table/sorting` — feature-level state shapes.
@@ -0,0 +1,230 @@
1
+ ---
2
+ name: preact/compose-with-tanstack-form
3
+ description: >
4
+ Editable cells with `@tanstack/preact-form`. The table is the layout
5
+ primitive; the form owns the state. Use `createFormHook` to register
6
+ reusable field components (`TextField`, `NumberField`, `SelectField`), and
7
+ in each column's `cell` renderer return the matching field component bound
8
+ to that row's accessor. Row identity (via `getRowId`) keeps field state
9
+ stable as rows resort / re-filter. Routing keywords: preact-form, editable
10
+ cells, inline editing, createFormHook, FieldGroup, getRowId.
11
+ type: composition
12
+ library: tanstack-table
13
+ framework: preact
14
+ library_version: '9.0.0-alpha.47'
15
+ requires:
16
+ - row-selection
17
+ - column-definitions
18
+ sources:
19
+ - TanStack/table:examples/react/with-tanstack-form/
20
+ - TanStack/table:docs/framework/preact/preact-table.md
21
+ ---
22
+
23
+ This skill is the Preact recipe for editable cells via @tanstack/preact-form. The Preact-Form API mirrors the React-Form API closely; the table half of the recipe is what you'd write in vanilla Preact + Table v9.
24
+
25
+ > **No dedicated examples/preact/with-tanstack-form yet** — the reference implementation lives under `examples/react/with-tanstack-form/` and ports line-for-line to Preact. The patterns below are the supported integration shape.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ npm install @tanstack/preact-form @tanstack/preact-table
31
+ ```
32
+
33
+ Peer dependency: `preact >=10`.
34
+
35
+ ## The Division of Labor
36
+
37
+ | Concern | Owner |
38
+ | ------------------------------------------------ | ------------------ |
39
+ | Layout (rows, columns, headers) | Table |
40
+ | Cell rendering API | Table |
41
+ | Sorting / filtering / pagination | Table |
42
+ | Row identity | Table (`getRowId`) |
43
+ | Field state (value, errors, touched, validation) | Form |
44
+ | Form-level submit handler | Form |
45
+
46
+ The table never owns cell values for the purposes of editing — it renders fields, the form holds the values, and on submit you read from the form snapshot.
47
+
48
+ ## Pattern — `createFormHook` + field-component cells
49
+
50
+ Define reusable field components once. Compose them with `createFormHook` to get a typed `useAppForm`. In each editable column's `cell` renderer, plug the field component into `form.AppField` keyed by the row id.
51
+
52
+ ```tsx
53
+ import { createFormHook, createFormHookContexts } from '@tanstack/preact-form'
54
+
55
+ // Field components (one-off — text input, number input, etc.).
56
+ function TextField({ field }) {
57
+ return (
58
+ <input
59
+ type="text"
60
+ value={field.state.value as string}
61
+ onInput={(e) => field.handleChange((e.target as HTMLInputElement).value)}
62
+ onBlur={field.handleBlur}
63
+ />
64
+ )
65
+ }
66
+
67
+ function NumberField({ field }) {
68
+ return (
69
+ <input
70
+ type="number"
71
+ value={field.state.value as number}
72
+ onInput={(e) =>
73
+ field.handleChange(Number((e.target as HTMLInputElement).value))
74
+ }
75
+ onBlur={field.handleBlur}
76
+ />
77
+ )
78
+ }
79
+
80
+ export const { fieldContext, formContext } = createFormHookContexts()
81
+
82
+ export const { useAppForm } = createFormHook({
83
+ fieldContext,
84
+ formContext,
85
+ fieldComponents: { TextField, NumberField },
86
+ formComponents: {},
87
+ })
88
+ ```
89
+
90
+ Use the form per-row, keyed by `row.id`. Tables built with `getRowId` keep the same row id across re-sorts and refilters, so the form state stays attached to the same logical row.
91
+
92
+ ```tsx
93
+ import {
94
+ createColumnHelper,
95
+ tableFeatures,
96
+ useTable,
97
+ } from '@tanstack/preact-table'
98
+ import { useAppForm } from './form-hook'
99
+
100
+ type Person = { id: string; firstName: string; age: number }
101
+
102
+ const _features = tableFeatures({})
103
+ const columnHelper = createColumnHelper<typeof _features, Person>()
104
+
105
+ function EditableTable({ data }: { data: Person[] }) {
106
+ const table = useTable({
107
+ _features,
108
+ _rowModels: {},
109
+ columns: columnHelper.columns([
110
+ columnHelper.accessor('firstName', {
111
+ header: 'First Name',
112
+ cell: ({ row, getValue }) => (
113
+ <RowFieldCell
114
+ rowId={row.id}
115
+ field="firstName"
116
+ defaultValue={getValue()}
117
+ kind="text"
118
+ />
119
+ ),
120
+ }),
121
+ columnHelper.accessor('age', {
122
+ header: 'Age',
123
+ cell: ({ row, getValue }) => (
124
+ <RowFieldCell
125
+ rowId={row.id}
126
+ field="age"
127
+ defaultValue={getValue()}
128
+ kind="number"
129
+ />
130
+ ),
131
+ }),
132
+ ]),
133
+ data,
134
+ getRowId: (row) => row.id, // CRITICAL — keeps form state attached to a row identity
135
+ })
136
+
137
+ return (
138
+ <table>
139
+ <tbody>
140
+ {table.getRowModel().rows.map((row) => (
141
+ <tr key={row.id}>
142
+ {row.getAllCells().map((cell) => (
143
+ <td key={cell.id}>
144
+ <table.FlexRender cell={cell} />
145
+ </td>
146
+ ))}
147
+ </tr>
148
+ ))}
149
+ </tbody>
150
+ </table>
151
+ )
152
+ }
153
+
154
+ // One small form per row.
155
+ function RowFieldCell({
156
+ rowId,
157
+ field,
158
+ defaultValue,
159
+ kind,
160
+ }: {
161
+ rowId: string
162
+ field: string
163
+ defaultValue: unknown
164
+ kind: 'text' | 'number'
165
+ }) {
166
+ const form = useAppForm({
167
+ defaultValues: { [field]: defaultValue },
168
+ onSubmit: async ({ value }) => {
169
+ await saveRow(rowId, value)
170
+ },
171
+ })
172
+
173
+ return (
174
+ <form.AppField name={field as never}>
175
+ {(f) => (kind === 'text' ? <f.TextField /> : <f.NumberField />)}
176
+ </form.AppField>
177
+ )
178
+ }
179
+ ```
180
+
181
+ The bulk-edit alternative is a single top-level form with `<form.Field name={\`rows.${row.id}.firstName\`}>` per cell. Either pattern works; pick whichever matches your save shape.
182
+
183
+ ## Common Mistakes
184
+
185
+ ### CRITICAL Forgetting `getRowId`
186
+
187
+ Wrong:
188
+
189
+ ```tsx
190
+ useTable({ _features, _rowModels: {}, columns, data /* no getRowId */ })
191
+ ```
192
+
193
+ Correct:
194
+
195
+ ```tsx
196
+ useTable({
197
+ _features,
198
+ _rowModels: {},
199
+ columns,
200
+ data,
201
+ getRowId: (row) => row.id,
202
+ })
203
+ ```
204
+
205
+ Without `getRowId`, the table assigns positional ids. Sorting or filtering changes row ids, and per-row form state ends up bound to a different logical row.
206
+ Source: `docs/framework/preact/guide/table-state.md`.
207
+
208
+ ### HIGH Storing editable values in the table state instead of the form
209
+
210
+ Wrong: putting per-cell drafts in `table.atoms` slices, or in `table.options.data`.
211
+
212
+ Correct: leave the table data immutable; let the form hold the in-flight values. Only update the table data on save (refresh from server or splice in the new row).
213
+
214
+ ### HIGH Re-rendering the entire table on every keystroke
215
+
216
+ Wrong: the top-level `useTable` selects every form draft state slice.
217
+
218
+ Correct: form field state is held in the form, not the table. The table re-renders only when actual table state changes. Field re-renders happen inside `<f.Field>` automatically.
219
+
220
+ ### MEDIUM Reimplementing form validation by hand
221
+
222
+ Wrong: ad hoc `onChange` per field with validation logic in each cell.
223
+
224
+ Correct: use `@tanstack/preact-form`'s validators (`validators: { onChange: schema }`). The form handles touched / dirty / error state; the table just renders the field.
225
+
226
+ ## See Also
227
+
228
+ - `tanstack-table/preact/table-state` — Subscribe / atoms / FlexRender.
229
+ - `tanstack-table/row-selection` — combining row selection with bulk edit.
230
+ - `tanstack-table/column-definitions` — column helper with TFeatures.