@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 +10 -0
- package/dist/reactivity.cjs +1 -0
- package/dist/reactivity.cjs.map +1 -1
- package/dist/reactivity.js +1 -0
- package/dist/reactivity.js.map +1 -1
- package/package.json +6 -4
- package/skills/preact/client-to-server/SKILL.md +294 -0
- package/skills/preact/compose-with-tanstack-form/SKILL.md +230 -0
- package/skills/preact/compose-with-tanstack-pacer/SKILL.md +186 -0
- package/skills/preact/compose-with-tanstack-query/SKILL.md +283 -0
- package/skills/preact/compose-with-tanstack-store/SKILL.md +263 -0
- package/skills/preact/compose-with-tanstack-virtual/SKILL.md +275 -0
- package/skills/preact/getting-started/SKILL.md +371 -0
- package/skills/preact/migrate-v8-to-v9/SKILL.md +322 -0
- package/skills/preact/production-readiness/SKILL.md +278 -0
- package/skills/preact/table-state/SKILL.md +432 -0
- package/skills/preact/table-state/references/advanced-state-patterns.md +93 -0
- package/src/reactivity.ts +1 -0
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!
|
package/dist/reactivity.cjs
CHANGED
|
@@ -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) => {
|
package/dist/reactivity.cjs.map
CHANGED
|
@@ -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"}
|
package/dist/reactivity.js
CHANGED
package/dist/reactivity.js.map
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|