@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.
@@ -0,0 +1,263 @@
1
+ ---
2
+ name: preact/compose-with-tanstack-store
3
+ description: >
4
+ `@tanstack/preact-table` v9 is built on TanStack Store. Each registered state
5
+ slice is an atom. The table exposes three reactive surfaces:
6
+ `table.atoms.<slice>` (per-slice readonly), `table.store` (flat readonly
7
+ view), and `table.baseAtoms.<slice>` (internal writable). Use external atoms
8
+ via `useCreateAtom` + `options.atoms` to hand slice ownership to your app,
9
+ share atoms across components with `useSelector`, and subscribe imperatively
10
+ with `atom.subscribe(...)` for persistence/sync. Routing keywords:
11
+ preact-store, useCreateAtom, useSelector, atoms, external state, slice
12
+ ownership, persistence.
13
+ type: composition
14
+ library: tanstack-table
15
+ framework: preact
16
+ library_version: '9.0.0-alpha.47'
17
+ requires:
18
+ - state-management
19
+ - preact/table-state
20
+ sources:
21
+ - TanStack/table:docs/framework/preact/guide/table-state.md
22
+ - TanStack/table:examples/preact/basic-external-atoms/src/main.tsx
23
+ - TanStack/table:examples/preact/basic-subscribe/src/main.tsx
24
+ - TanStack/table:packages/preact-table/src/useTable.ts
25
+ - TanStack/table:packages/preact-table/src/reactivity.ts
26
+ ---
27
+
28
+ `@tanstack/preact-table` v9 stores every registered state slice as a TanStack Store atom under the hood, and `useTable` wires Preact to those atoms via `@tanstack/preact-store`. This skill is the bridge between table state and the rest of your TanStack Store-powered app.
29
+
30
+ ## The Three Surfaces
31
+
32
+ | Surface | Shape | Use when |
33
+ | ------------------------- | ---------------------------------------------------------- | --------------------------------------------------- |
34
+ | `table.atoms.<slice>` | `ReadonlyAtom<TSliceState>` per slice | Read or subscribe to one slice (preferred) |
35
+ | `table.store` | `ReadonlyStore<TableState<TFeatures>>` (flat derived view) | Reading full table state, selecting projections |
36
+ | `table.baseAtoms.<slice>` | `Atom<TSliceState>` (writable internal) | Low-level writes when the slice is internally owned |
37
+
38
+ External atoms passed via `options.atoms.<slice>` take precedence over `options.state[<slice>]` and over `table.baseAtoms.<slice>`. Writes from feature APIs (`table.setSorting(...)`, `table.setPageIndex(...)`, etc.) flow into whichever atom owns the slice.
39
+
40
+ Source: `docs/framework/preact/guide/table-state.md`; `packages/preact-table/src/useTable.ts`.
41
+
42
+ ## Pattern 1 — Hand a slice to your app
43
+
44
+ ```tsx
45
+ import { useCreateAtom, useSelector } from '@tanstack/preact-store'
46
+ import {
47
+ rowPaginationFeature,
48
+ rowSortingFeature,
49
+ tableFeatures,
50
+ useTable,
51
+ type PaginationState,
52
+ type SortingState,
53
+ } from '@tanstack/preact-table'
54
+
55
+ const _features = tableFeatures({ rowPaginationFeature, rowSortingFeature })
56
+
57
+ function PeopleTable({ data }) {
58
+ // Stable atoms owned by this component.
59
+ const sortingAtom = useCreateAtom<SortingState>([])
60
+ const paginationAtom = useCreateAtom<PaginationState>({
61
+ pageIndex: 0,
62
+ pageSize: 10,
63
+ })
64
+
65
+ // Independent reactive reads — only re-renders for the slice that changed.
66
+ const sorting = useSelector(sortingAtom)
67
+ const pagination = useSelector(paginationAtom)
68
+
69
+ const table = useTable({
70
+ _features,
71
+ _rowModels: {
72
+ /* … */
73
+ },
74
+ columns,
75
+ data,
76
+ atoms: { sorting: sortingAtom, pagination: paginationAtom },
77
+ })
78
+
79
+ // table.setSorting / table.setPageIndex write to your atoms.
80
+ return null
81
+ }
82
+ ```
83
+
84
+ Source: `examples/preact/basic-external-atoms/src/main.tsx`.
85
+
86
+ ## Pattern 2 — Share an atom across components
87
+
88
+ Lift the atom to a module / context. Any component can read or write it, and the table stays in sync.
89
+
90
+ ```tsx
91
+ // shared/atoms.ts
92
+ import { createAtom } from '@tanstack/preact-store'
93
+ import type { RowSelectionState } from '@tanstack/preact-table'
94
+
95
+ export const selectionAtom = createAtom<RowSelectionState>({})
96
+ ```
97
+
98
+ ```tsx
99
+ // TableScreen.tsx
100
+ import { selectionAtom } from '../shared/atoms'
101
+
102
+ const table = useTable({
103
+ _features,
104
+ _rowModels: {},
105
+ columns,
106
+ data,
107
+ atoms: { rowSelection: selectionAtom },
108
+ })
109
+ ```
110
+
111
+ ```tsx
112
+ // SelectionSummary.tsx
113
+ import { useSelector } from '@tanstack/preact-store'
114
+ import { selectionAtom } from '../shared/atoms'
115
+
116
+ function SelectionSummary() {
117
+ const sel = useSelector(selectionAtom)
118
+ return <span>{Object.keys(sel).length} selected</span>
119
+ }
120
+ ```
121
+
122
+ Module-scope atoms are stable identities — no `useCreateAtom` needed. Don't create module-scope atoms inside a component-render body.
123
+
124
+ Source: `docs/framework/preact/guide/table-state.md`.
125
+
126
+ ## Pattern 3 — Subscribe imperatively for persistence / sync
127
+
128
+ `Atom.subscribe` returns an `{ unsubscribe }` handle. Persist to `localStorage`, push to a URL, or fan out to other stores.
129
+
130
+ ```tsx
131
+ import { useEffect } from 'preact/hooks'
132
+
133
+ useEffect(() => {
134
+ const sub = paginationAtom.subscribe((next) => {
135
+ localStorage.setItem('table:pagination', JSON.stringify(next))
136
+ })
137
+ return () => sub.unsubscribe()
138
+ }, [paginationAtom])
139
+ ```
140
+
141
+ You can also subscribe to `table.atoms.<slice>` directly without owning the slice. The subscription fires whenever the slice changes — whoever owns it (your atom or `baseAtoms`).
142
+
143
+ Source: `packages/preact-table/src/reactivity.ts`.
144
+
145
+ ## Pattern 4 — Read inside cells with the standalone `<Subscribe>`
146
+
147
+ Inside a cell or header render context, `table` is the core `Table<TFeatures, TData>`, not `PreactTable`. `table.Subscribe` is undefined — import the standalone component.
148
+
149
+ ```tsx
150
+ import { Subscribe } from '@tanstack/preact-table'
151
+
152
+ columnHelper.display({
153
+ id: 'select',
154
+ cell: ({ row, table }) => (
155
+ <Subscribe source={table.atoms.rowSelection} selector={(rs) => rs[row.id]}>
156
+ {(isSelected) => (
157
+ <input
158
+ type="checkbox"
159
+ checked={!!isSelected}
160
+ onChange={row.getToggleSelectedHandler()}
161
+ />
162
+ )}
163
+ </Subscribe>
164
+ ),
165
+ })
166
+ ```
167
+
168
+ Source: `packages/preact-table/src/Subscribe.ts`; `examples/preact/basic-subscribe/src/main.tsx`.
169
+
170
+ ## Common Mistakes
171
+
172
+ ### CRITICAL Creating an atom inside the render body without `useCreateAtom`
173
+
174
+ Wrong:
175
+
176
+ ```tsx
177
+ function MyTable() {
178
+ const sortingAtom = createAtom<SortingState>([]) // new atom every render
179
+ useTable({ /* … */, atoms: { sorting: sortingAtom } })
180
+ }
181
+ ```
182
+
183
+ Correct:
184
+
185
+ ```tsx
186
+ function MyTable() {
187
+ const sortingAtom = useCreateAtom<SortingState>([]) // stable
188
+ useTable({ /* … */, atoms: { sorting: sortingAtom } })
189
+ }
190
+ ```
191
+
192
+ A fresh atom each render unbinds the slice and resets it to the initial value on every render.
193
+ Source: `examples/preact/basic-external-atoms/src/main.tsx`.
194
+
195
+ ### HIGH Writing to `table.baseAtoms.X.set()` when the slice is externally owned
196
+
197
+ Wrong:
198
+
199
+ ```tsx
200
+ useTable({ /* … */, atoms: { pagination: paginationAtom } })
201
+ table.baseAtoms.pagination.set((old) => ({ ...old, pageIndex: 0 })) // updates the wrong atom
202
+ ```
203
+
204
+ Correct:
205
+
206
+ ```tsx
207
+ // Use the feature API (writes to whichever atom owns the slice).
208
+ table.setPageIndex(0)
209
+ // Or write to your external atom directly.
210
+ paginationAtom.set((old) => ({ ...old, pageIndex: 0 }))
211
+ ```
212
+
213
+ `table.baseAtoms` is the internal writable atom — used only when the slice is internally owned. When you hand a slice to an external atom, the external atom is the source of truth.
214
+ Source: `docs/framework/preact/guide/table-state.md`.
215
+
216
+ ### HIGH Reading `.get()` and expecting re-renders
217
+
218
+ Wrong:
219
+
220
+ ```tsx
221
+ function Pager() {
222
+ const { pageIndex } = table.atoms.pagination.get() // current-value read
223
+ return <span>Page {pageIndex + 1}</span>
224
+ }
225
+ ```
226
+
227
+ Correct:
228
+
229
+ ```tsx
230
+ import { useSelector } from '@tanstack/preact-store'
231
+
232
+ function Pager() {
233
+ const pageIndex = useSelector(table.atoms.pagination, (p) => p.pageIndex)
234
+ return <span>Page {pageIndex + 1}</span>
235
+ }
236
+ // or
237
+ ;<table.Subscribe source={table.atoms.pagination} selector={(p) => p.pageIndex}>
238
+ {(pageIndex) => <span>Page {pageIndex + 1}</span>}
239
+ </table.Subscribe>
240
+ ```
241
+
242
+ `.get()` returns the current value without subscribing.
243
+
244
+ ### MEDIUM Passing the same slice via `atoms` AND `state`
245
+
246
+ Wrong:
247
+
248
+ ```tsx
249
+ useTable({
250
+ /* … */,
251
+ atoms: { pagination: paginationAtom },
252
+ state: { pagination }, // silently ignored
253
+ onPaginationChange: setPagination, // silently ignored
254
+ })
255
+ ```
256
+
257
+ Correct: pick exactly one ownership path per slice.
258
+
259
+ ## See Also
260
+
261
+ - `tanstack-table/preact/table-state` — atoms / Subscribe / FlexRender reference.
262
+ - `tanstack-table/preact/compose-with-tanstack-query` — server-side flow keyed by atoms.
263
+ - `tanstack-table/preact/production-readiness` — when to reach for narrow subscriptions.
@@ -0,0 +1,275 @@
1
+ ---
2
+ name: preact/compose-with-tanstack-virtual
3
+ description: >
4
+ TanStack Table does NOT include virtualization — pair with TanStack Virtual.
5
+ Preact has no dedicated `@tanstack/preact-virtual` adapter yet; use
6
+ `@tanstack/virtual-core`'s `Virtualizer` class behind a small hook, or use
7
+ the React adapter via `preact/compat`. Pattern: get `rows = table.getRowModel().rows`,
8
+ feed `rows.length` to the virtualizer, render only virtual items, and use
9
+ CSS transforms for row positioning. Routing keywords: preact virtualization,
10
+ large table, virtual rows, virtual-core, getVirtualItems, table-core.
11
+ type: composition
12
+ library: tanstack-table
13
+ framework: preact
14
+ library_version: '9.0.0-alpha.47'
15
+ requires:
16
+ - preact/table-state
17
+ - row-expanding
18
+ sources:
19
+ - TanStack/table:docs/guide/virtualization.md
20
+ - TanStack/table:examples/lit/virtualized-rows/src/main.ts
21
+ - TanStack/table:examples/react/virtualized-rows/
22
+ - TanStack/table:examples/react/virtualized-columns/
23
+ ---
24
+
25
+ TanStack Table is headless — it does not virtualize rows or columns. For long lists, pair the table with TanStack Virtual.
26
+
27
+ > **Adapter status:** There is no published `@tanstack/preact-virtual` adapter as of `@tanstack/table` v9.0.0-alpha.47. The two supported paths are:
28
+ >
29
+ > 1. **Use `@tanstack/virtual-core` directly.** The framework-agnostic `Virtualizer` class wrapped in a small Preact hook is the recommended path.
30
+ > 2. **Use `@tanstack/react-virtual` via `preact/compat`.** Works if your project already aliases `react` → `preact/compat`.
31
+ >
32
+ > The patterns below use path 1. Both paths feed the same `rows = table.getRowModel().rows` array to the virtualizer.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ npm install @tanstack/virtual-core
38
+ ```
39
+
40
+ ## The Pattern (Row Virtualization)
41
+
42
+ 1. Build the table with `useTable` as usual.
43
+ 2. Get `const { rows } = table.getRowModel()` — the table is the source of truth for which rows to render (already sorted, filtered, paginated, etc.).
44
+ 3. Pass `rows.length` to the virtualizer.
45
+ 4. Render only `virtualizer.getVirtualItems()`.
46
+ 5. Each virtual row is absolutely positioned via `transform: translateY(${item.start}px)`.
47
+
48
+ ```tsx
49
+ import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
50
+ import {
51
+ Virtualizer,
52
+ observeElementOffset,
53
+ observeElementRect,
54
+ elementScroll,
55
+ } from '@tanstack/virtual-core'
56
+ import {
57
+ useTable,
58
+ createSortedRowModel,
59
+ rowSortingFeature,
60
+ sortFns,
61
+ tableFeatures,
62
+ } from '@tanstack/preact-table'
63
+ import type { JSX } from 'preact'
64
+
65
+ const _features = tableFeatures({ rowSortingFeature })
66
+
67
+ // Minimal Preact hook around the framework-agnostic Virtualizer.
68
+ function useVirtualizer<TScrollEl extends Element, TItemEl extends Element>(
69
+ options: Omit<
70
+ ConstructorParameters<typeof Virtualizer<TScrollEl, TItemEl>>[0],
71
+ 'observeElementRect' | 'observeElementOffset' | 'scrollToFn'
72
+ > & { onChange?: (instance: Virtualizer<TScrollEl, TItemEl>) => void },
73
+ ) {
74
+ const [, force] = useState(0)
75
+ const virtualizer = useMemo(
76
+ () =>
77
+ new Virtualizer<TScrollEl, TItemEl>({
78
+ ...options,
79
+ observeElementRect,
80
+ observeElementOffset,
81
+ scrollToFn: elementScroll,
82
+ onChange: (inst) => {
83
+ options.onChange?.(inst)
84
+ force((n) => n + 1)
85
+ },
86
+ }),
87
+ [],
88
+ )
89
+
90
+ // Sync count / estimateSize / overscan when they change.
91
+ virtualizer.setOptions({
92
+ ...virtualizer.options,
93
+ count: options.count,
94
+ estimateSize: options.estimateSize,
95
+ overscan: options.overscan,
96
+ })
97
+
98
+ useEffect(() => {
99
+ return virtualizer._didMount()
100
+ }, [virtualizer])
101
+
102
+ useEffect(() => {
103
+ virtualizer._willUpdate()
104
+ })
105
+
106
+ return virtualizer
107
+ }
108
+
109
+ function BigTable({ data }) {
110
+ const table = useTable(
111
+ {
112
+ _features,
113
+ _rowModels: { sortedRowModel: createSortedRowModel(sortFns) },
114
+ columns,
115
+ data,
116
+ },
117
+ () => null, // huge table — opt out at the top, subscribe lower down
118
+ )
119
+
120
+ const { rows } = table.getRowModel()
121
+
122
+ const scrollRef = useRef<HTMLDivElement>(null)
123
+
124
+ const rowVirtualizer = useVirtualizer({
125
+ count: rows.length,
126
+ getScrollElement: () => scrollRef.current!,
127
+ estimateSize: () => 33,
128
+ overscan: 5,
129
+ })
130
+
131
+ return (
132
+ <div
133
+ ref={scrollRef}
134
+ style={{ overflow: 'auto', height: 800, position: 'relative' }}
135
+ >
136
+ <table style={{ display: 'grid' }}>
137
+ <thead
138
+ style={{ display: 'grid', position: 'sticky', top: 0, zIndex: 1 }}
139
+ >
140
+ {table.getHeaderGroups().map((hg) => (
141
+ <tr key={hg.id} style={{ display: 'flex', width: '100%' }}>
142
+ {hg.headers.map((h) => (
143
+ <th key={h.id} style={{ display: 'flex', width: h.getSize() }}>
144
+ <table.FlexRender header={h} />
145
+ </th>
146
+ ))}
147
+ </tr>
148
+ ))}
149
+ </thead>
150
+
151
+ <tbody
152
+ style={{
153
+ display: 'grid',
154
+ height: `${rowVirtualizer.getTotalSize()}px`,
155
+ position: 'relative',
156
+ }}
157
+ >
158
+ {rowVirtualizer.getVirtualItems().map((virtualItem) => {
159
+ const row = rows[virtualItem.index]
160
+ return (
161
+ <tr
162
+ key={row.id}
163
+ ref={(node) => rowVirtualizer.measureElement(node!)}
164
+ data-index={virtualItem.index}
165
+ style={{
166
+ display: 'flex',
167
+ position: 'absolute',
168
+ transform: `translateY(${virtualItem.start}px)`,
169
+ width: '100%',
170
+ }}
171
+ >
172
+ {row.getAllCells().map((cell) => (
173
+ <td
174
+ key={cell.id}
175
+ style={{ display: 'flex', width: cell.column.getSize() }}
176
+ >
177
+ <table.FlexRender cell={cell} />
178
+ </td>
179
+ ))}
180
+ </tr>
181
+ )
182
+ })}
183
+ </tbody>
184
+ </table>
185
+ </div>
186
+ )
187
+ }
188
+ ```
189
+
190
+ The structure matches the Lit virtualized-rows example one-for-one; only the host framework changes.
191
+ Source: `examples/lit/virtualized-rows/src/main.ts`; `docs/guide/virtualization.md`.
192
+
193
+ ## Column Virtualization
194
+
195
+ Same idea, but the virtualizer's `count` is `columns.length` and you index the visible columns inside each row. Useful for wide kitchen-sink tables.
196
+
197
+ ## With Pagination / Filtering
198
+
199
+ Use the row model that's already been transformed by registered features. The virtualizer count is `rows.length` — the table handles sorting, filtering, and pagination upstream.
200
+
201
+ ## Combining with `<table.Subscribe>`
202
+
203
+ On large tables, pass `() => null` to `useTable` (or use the standalone `<Subscribe>`) and wrap the `<tbody>` in a subscription that re-renders only when the row model can actually change.
204
+
205
+ ```tsx
206
+ <table.Subscribe
207
+ selector={(s) => ({
208
+ columnFilters: s.columnFilters,
209
+ globalFilter: s.globalFilter,
210
+ sorting: s.sorting,
211
+ })}
212
+ >
213
+ {() => <tbody>{/* virtualized rows */}</tbody>}
214
+ </table.Subscribe>
215
+ ```
216
+
217
+ Source: `examples/preact/basic-subscribe/src/main.tsx`.
218
+
219
+ ## Common Mistakes
220
+
221
+ ### CRITICAL Reimplementing virtualization by hand
222
+
223
+ Wrong:
224
+
225
+ ```tsx
226
+ // Manual slicing + intersection observer + per-row offset calculation
227
+ ```
228
+
229
+ Correct: use TanStack Virtual's `Virtualizer` — it handles measurement, overscan, scroll alignment, and dynamic sizing.
230
+ Source: `docs/guide/virtualization.md`.
231
+
232
+ ### HIGH Using the wrong row source
233
+
234
+ Wrong:
235
+
236
+ ```tsx
237
+ const virtualizer = useVirtualizer({ count: data.length /* … */ }) // bypasses sort/filter/paginate
238
+ ```
239
+
240
+ Correct:
241
+
242
+ ```tsx
243
+ const { rows } = table.getRowModel()
244
+ const virtualizer = useVirtualizer({ count: rows.length /* … */ })
245
+ ```
246
+
247
+ Always feed `table.getRowModel().rows.length` — that's the post-feature row array.
248
+
249
+ ### HIGH Forgetting `position: relative` on the scroll parent / `position: absolute` on rows
250
+
251
+ Wrong:
252
+
253
+ ```tsx
254
+ <div ref={scrollRef} style={{ overflow: 'auto', height: 800 }}>
255
+ <tbody style={{ height: rowVirtualizer.getTotalSize() }}>{/* rows */}</tbody>
256
+ </div>
257
+ ```
258
+
259
+ Correct: the absolute rows need a `position: relative` ancestor with the total height set. Without it, rows stack at the top.
260
+
261
+ ### HIGH Forgetting to set up `measureElement` for dynamic sizing
262
+
263
+ Wrong: rows render but `estimateSize` is wrong and rows overlap or leave gaps.
264
+
265
+ Correct: attach `ref={(node) => rowVirtualizer.measureElement(node!)}` to each row element so the virtualizer can measure actual size.
266
+
267
+ ### MEDIUM Mixing virtualization with `manualPagination`
268
+
269
+ You usually don't need both — server pagination already limits the row count. Virtualize when the client holds the full dataset.
270
+
271
+ ## See Also
272
+
273
+ - `tanstack-table/preact/table-state` — Subscribe for fine-grained re-renders.
274
+ - `tanstack-table/preact/production-readiness` — narrow selectors, stable refs.
275
+ - `tanstack-table/row-expanding` — virtualizing rows with sub-component rows requires variable height + measureElement.