@tanstack/react-table 9.0.0-alpha.8 → 9.0.0-beta.1
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 +127 -0
- package/dist/FlexRender.cjs +61 -0
- package/dist/FlexRender.cjs.map +1 -0
- package/dist/FlexRender.d.cts +51 -0
- package/dist/FlexRender.d.ts +51 -0
- package/dist/FlexRender.js +58 -0
- package/dist/FlexRender.js.map +1 -0
- package/dist/Subscribe.cjs +13 -0
- package/dist/Subscribe.cjs.map +1 -0
- package/dist/Subscribe.d.cts +101 -0
- package/dist/Subscribe.d.ts +101 -0
- package/dist/Subscribe.js +13 -0
- package/dist/Subscribe.js.map +1 -0
- package/dist/_virtual/_rolldown/runtime.cjs +29 -0
- package/dist/createTableHook.cjs +313 -0
- package/dist/createTableHook.cjs.map +1 -0
- package/dist/createTableHook.d.cts +358 -0
- package/dist/createTableHook.d.ts +358 -0
- package/dist/createTableHook.js +311 -0
- package/dist/createTableHook.js.map +1 -0
- package/dist/flex-render.cjs +5 -0
- package/dist/flex-render.d.cts +2 -0
- package/dist/flex-render.d.ts +2 -0
- package/dist/flex-render.js +3 -0
- package/dist/index.cjs +18 -0
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +8 -0
- package/dist/legacy.cjs +14 -0
- package/dist/legacy.d.cts +2 -0
- package/dist/legacy.d.ts +2 -0
- package/dist/legacy.js +3 -0
- package/dist/reactivity.cjs +34 -0
- package/dist/reactivity.cjs.map +1 -0
- package/dist/reactivity.js +34 -0
- package/dist/reactivity.js.map +1 -0
- package/dist/static-functions.cjs +9 -0
- package/dist/static-functions.d.cts +1 -0
- package/dist/static-functions.d.ts +1 -0
- package/dist/static-functions.js +3 -0
- package/dist/useLegacyTable.cjs +191 -0
- package/dist/useLegacyTable.cjs.map +1 -0
- package/dist/useLegacyTable.d.cts +233 -0
- package/dist/useLegacyTable.d.ts +233 -0
- package/dist/useLegacyTable.js +181 -0
- package/dist/useLegacyTable.js.map +1 -0
- package/dist/useTable.cjs +72 -0
- package/dist/useTable.cjs.map +1 -0
- package/dist/useTable.d.cts +122 -0
- package/dist/useTable.d.ts +122 -0
- package/dist/useTable.js +72 -0
- package/dist/useTable.js.map +1 -0
- package/package.json +41 -22
- package/skills/react/client-to-server/SKILL.md +377 -0
- package/skills/react/compose-with-tanstack-form/SKILL.md +363 -0
- package/skills/react/compose-with-tanstack-pacer/SKILL.md +287 -0
- package/skills/react/compose-with-tanstack-query/SKILL.md +467 -0
- package/skills/react/compose-with-tanstack-store/SKILL.md +347 -0
- package/skills/react/compose-with-tanstack-virtual/SKILL.md +388 -0
- package/skills/react/compose-with-tanstack-virtual/references/column-virtualization-and-infinite-scroll.md +136 -0
- package/skills/react/getting-started/SKILL.md +388 -0
- package/skills/react/migrate-v8-to-v9/SKILL.md +488 -0
- package/skills/react/production-readiness/SKILL.md +341 -0
- package/skills/react/react-subscribe-compiler-compat/SKILL.md +269 -0
- package/skills/react/table-state/SKILL.md +432 -0
- package/src/FlexRender.tsx +136 -0
- package/src/Subscribe.ts +153 -0
- package/src/createTableHook.tsx +1121 -0
- package/src/flex-render.ts +1 -0
- package/src/index.ts +6 -0
- package/src/legacy.ts +3 -0
- package/src/reactivity.ts +41 -0
- package/src/static-functions.ts +1 -0
- package/src/useLegacyTable.ts +487 -0
- package/src/useTable.ts +191 -0
- package/dist/cjs/index.cjs +0 -77
- package/dist/cjs/index.cjs.map +0 -1
- package/dist/cjs/index.d.cts +0 -9
- package/dist/esm/index.d.ts +0 -9
- package/dist/esm/index.js +0 -55
- package/dist/esm/index.js.map +0 -1
- package/src/index.tsx +0 -92
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react/production-readiness
|
|
3
|
+
description: >
|
|
4
|
+
Ship-ready optimizations for `@tanstack/react-table` v9: tree-shake the
|
|
5
|
+
bundle by registering ONLY the `features` you actually use; memoize
|
|
6
|
+
`features`, `data`, and `columns` for stable identity; replace
|
|
7
|
+
`(state) => state` with narrow selectors or per-slice
|
|
8
|
+
`useSelector(table.atoms.<slice>)` subscriptions; and push state-driven
|
|
9
|
+
re-renders down the tree with `<table.Subscribe>` / `<Subscribe>` so the
|
|
10
|
+
expensive table body doesn't re-render every time you toggle a sort
|
|
11
|
+
indicator. Don't over-optimize small tables — the default selector +
|
|
12
|
+
inline rendering is fine until measured perf demands more.
|
|
13
|
+
type: lifecycle
|
|
14
|
+
library: tanstack-table
|
|
15
|
+
framework: react
|
|
16
|
+
library_version: '9.0.0-alpha.48'
|
|
17
|
+
requires:
|
|
18
|
+
- setup
|
|
19
|
+
- state-management
|
|
20
|
+
- react/table-state
|
|
21
|
+
sources:
|
|
22
|
+
- TanStack/table:docs/guide/features.md
|
|
23
|
+
- TanStack/table:docs/framework/react/guide/table-state.md
|
|
24
|
+
- TanStack/table:examples/react/basic-subscribe/src/main.tsx
|
|
25
|
+
- TanStack/table:examples/react/basic-external-atoms/src/main.tsx
|
|
26
|
+
- TanStack/table:examples/react/kitchen-sink/src/main.tsx
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
This skill builds on `tanstack-table/state-management` and `tanstack-table/react/table-state`. Read those first — `features` tree-shaking and the atom reactivity model are the foundation; this skill is about _which_ of the patterns introduced there you actually need in production.
|
|
30
|
+
|
|
31
|
+
## When to optimize
|
|
32
|
+
|
|
33
|
+
The default `useTable` selector is `(state) => state` — the component re-renders on any state change. That's correct and ergonomic, and for tables with a few hundred rows and basic features it's the right default. Don't reach for `<Subscribe>` walls or per-slice atom subscriptions until you've **measured** a problem (slow keystrokes in a filter input, dropped frames during scrolling, long-running renders). On small tables the optimization noise costs more than it saves.
|
|
34
|
+
|
|
35
|
+
## Setup — stable references
|
|
36
|
+
|
|
37
|
+
The biggest single perf win is keeping `features`, `rowModels`, `columns`, and `data` references stable across renders. Internal memoization keys off identity, so a new object every render forces full recomputation.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// ✓ Module scope = stable identity
|
|
41
|
+
const features = tableFeatures({ rowSortingFeature, rowPaginationFeature })
|
|
42
|
+
const rowModels = {
|
|
43
|
+
sortedRowModel: createSortedRowModel(sortFns),
|
|
44
|
+
paginatedRowModel: createPaginatedRowModel(),
|
|
45
|
+
}
|
|
46
|
+
const columnHelper = createColumnHelper<typeof features, Person>()
|
|
47
|
+
const columns = columnHelper.columns([
|
|
48
|
+
columnHelper.accessor('firstName', { header: 'First' }),
|
|
49
|
+
columnHelper.accessor('age', { header: 'Age' }),
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
// Module-scope empty array for the "no data yet" branch.
|
|
53
|
+
const EMPTY: Person[] = []
|
|
54
|
+
|
|
55
|
+
function MyTable({ data }: { data: Person[] | undefined }) {
|
|
56
|
+
const table = useTable({
|
|
57
|
+
features,
|
|
58
|
+
rowModels,
|
|
59
|
+
columns,
|
|
60
|
+
data: data ?? EMPTY,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Core Patterns
|
|
66
|
+
|
|
67
|
+
### 1. Tree-shake `features` to only what you use
|
|
68
|
+
|
|
69
|
+
Avoid `stockFeatures` in production. A sort-only table is ~6–7kb registered explicitly versus ~15–20kb if you import the whole stock set.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// ✓ Pay only for what you render
|
|
73
|
+
const features = tableFeatures({
|
|
74
|
+
rowSortingFeature,
|
|
75
|
+
rowPaginationFeature,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// ✗ Ships filtering, faceting, grouping, pinning, expanding, sizing,
|
|
79
|
+
// resizing, visibility, ordering, row-selection, row-pinning…
|
|
80
|
+
const features = tableFeatures(stockFeatures)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Source: `docs/guide/features.md`; maintainer guidance.
|
|
84
|
+
|
|
85
|
+
### 2. Narrow the `useTable` selector
|
|
86
|
+
|
|
87
|
+
`(state) => state` re-renders the holding component on any state change. If only one component cares about one slice, pass a narrow selector — or pass `() => null` and rely on `<table.Subscribe>` walls inside.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// Narrow to specific slices at the table level.
|
|
91
|
+
const table = useTable({ features, rowModels, columns, data }, (state) => ({
|
|
92
|
+
sorting: state.sorting,
|
|
93
|
+
pagination: state.pagination,
|
|
94
|
+
}))
|
|
95
|
+
|
|
96
|
+
// Or: opt out completely at the top, subscribe surgically inside.
|
|
97
|
+
const table = useTable(opts, () => null)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Source: `examples/react/basic-subscribe/src/main.tsx` (uses `() => null`).
|
|
101
|
+
|
|
102
|
+
### 3. Push re-renders down with `<table.Subscribe>`
|
|
103
|
+
|
|
104
|
+
A noisy footer that re-renders on every keystroke in a filter doesn't need to re-render the whole `<TableBody>`. Wrap each consumer in `<table.Subscribe>` with its own selector.
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
function MyTable({ data, columns }) {
|
|
108
|
+
const table = useTable(
|
|
109
|
+
{ features, rowModels, columns, data },
|
|
110
|
+
() => null, // top-level opt-out
|
|
111
|
+
)
|
|
112
|
+
return (
|
|
113
|
+
<>
|
|
114
|
+
<TableBody table={table} /> {/* heavy — keep stable */}
|
|
115
|
+
{/* Footer re-renders only on pagination changes */}
|
|
116
|
+
<table.Subscribe selector={(s) => s.pagination}>
|
|
117
|
+
{(pagination) => <PageFooter pagination={pagination} table={table} />}
|
|
118
|
+
</table.Subscribe>
|
|
119
|
+
</>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Source: `examples/react/basic-subscribe/src/main.tsx`.
|
|
125
|
+
|
|
126
|
+
### 4. Per-slice `useSelector(table.atoms.<slice>)` for narrowest scope
|
|
127
|
+
|
|
128
|
+
Even narrower than `<table.Subscribe>`: subscribe a leaf component to a single atom. Skips constructing a state snapshot entirely.
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { useSelector } from '@tanstack/react-store'
|
|
132
|
+
|
|
133
|
+
function SelectedCount({ table }) {
|
|
134
|
+
// Re-renders ONLY when rowSelection changes — not sorting / pagination / etc.
|
|
135
|
+
const selection = useSelector(table.atoms.rowSelection)
|
|
136
|
+
return <span>{Object.keys(selection).length} selected</span>
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Source: `examples/react/basic-external-atoms/src/main.tsx`.
|
|
141
|
+
|
|
142
|
+
### 5. React Compiler — read state via `<Subscribe>` in nested components
|
|
143
|
+
|
|
144
|
+
The compiler can't see through the `table` closure, so reads via builder APIs (`column.getIsPinned()`, `row.getIsSelected()`) in memoized child components go stale. Wrap them in `<Subscribe>` (see `tanstack-table/react/react-subscribe-compiler-compat`).
|
|
145
|
+
|
|
146
|
+
### 6. Virtualization in the deepest possible component
|
|
147
|
+
|
|
148
|
+
Keep `useVirtualizer` in the deepest component (`TableBody`, not `App`). Any state change in the holder of the virtualizer re-runs it and tanks scroll perf. See `tanstack-table/react/compose-with-tanstack-virtual`.
|
|
149
|
+
|
|
150
|
+
## Common Mistakes
|
|
151
|
+
|
|
152
|
+
### HIGH Using `stockFeatures` in production
|
|
153
|
+
|
|
154
|
+
Wrong:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { useTable, stockFeatures, tableFeatures } from '@tanstack/react-table'
|
|
158
|
+
const features = tableFeatures(stockFeatures) // ships every feature
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Correct:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import {
|
|
165
|
+
useTable,
|
|
166
|
+
tableFeatures,
|
|
167
|
+
rowSortingFeature,
|
|
168
|
+
rowPaginationFeature,
|
|
169
|
+
} from '@tanstack/react-table'
|
|
170
|
+
const features = tableFeatures({ rowSortingFeature, rowPaginationFeature })
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Tree-shaking via `features` is one of the headline reasons for the v9 rewrite. `stockFeatures` exists for migration / "everything on" smoke tests, not production.
|
|
174
|
+
Source: maintainer guidance; `docs/guide/features.md`.
|
|
175
|
+
|
|
176
|
+
### HIGH Unstable `features` / `rowModels` / `columns` references
|
|
177
|
+
|
|
178
|
+
Wrong:
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
function MyTable({ data }) {
|
|
182
|
+
const features = tableFeatures({ rowSortingFeature }) // new every render
|
|
183
|
+
const rowModels = { sortedRowModel: createSortedRowModel(sortFns) } // new every render
|
|
184
|
+
const table = useTable({ features, rowModels, columns, data })
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Correct:
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
// Module scope — declared once.
|
|
192
|
+
const features = tableFeatures({ rowSortingFeature })
|
|
193
|
+
const rowModels = { sortedRowModel: createSortedRowModel(sortFns) }
|
|
194
|
+
|
|
195
|
+
function MyTable({ data }) {
|
|
196
|
+
const table = useTable({ features, rowModels, columns, data })
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Internal memoization keys off identity. A new object every render busts memos and forces full recomputation.
|
|
201
|
+
Source: FAQ #1; `examples/react/basic-use-table/src/main.tsx`.
|
|
202
|
+
|
|
203
|
+
### HIGH `data={rows ?? []}` in JSX
|
|
204
|
+
|
|
205
|
+
Wrong:
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
<MyTable data={query.data?.rows ?? []} columns={columns} />
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Correct:
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
const EMPTY: Person[] = [] // module scope
|
|
215
|
+
|
|
216
|
+
<MyTable data={query.data?.rows ?? EMPTY} columns={columns} />
|
|
217
|
+
// or memoize the fallback:
|
|
218
|
+
const data = React.useMemo(() => query.data?.rows ?? [], [query.data])
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The `?? []` produces a new array identity each render, busting internal memos that depend on `data` reference.
|
|
222
|
+
Source: `examples/react/with-tanstack-query/src/main.tsx`.
|
|
223
|
+
|
|
224
|
+
### MEDIUM Leaving `(state) => state` when only one component cares
|
|
225
|
+
|
|
226
|
+
Wrong:
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
// Default selector — whole tree re-renders on every state change.
|
|
230
|
+
const table = useTable(opts)
|
|
231
|
+
return <DeeplyNestedTable table={table} />
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Correct:
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
const table = useTable(opts, () => null)
|
|
238
|
+
return (
|
|
239
|
+
<>
|
|
240
|
+
<TableBody table={table} />
|
|
241
|
+
<table.Subscribe selector={(s) => s.pagination}>
|
|
242
|
+
{(p) => <PageFooter pagination={p} />}
|
|
243
|
+
</table.Subscribe>
|
|
244
|
+
</>
|
|
245
|
+
)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Once you've measured a problem, narrow the top selector and add `<table.Subscribe>` walls around the components that actually need state.
|
|
249
|
+
Source: `examples/react/basic-subscribe/src/main.tsx`.
|
|
250
|
+
|
|
251
|
+
### MEDIUM Subscribing to the whole `table.store` when a single atom would do
|
|
252
|
+
|
|
253
|
+
Wrong:
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
<table.Subscribe selector={(s) => s.rowSelection}>
|
|
257
|
+
{(rs) => <span>{Object.keys(rs).length} selected</span>}
|
|
258
|
+
</table.Subscribe>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Correct:
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
import { useSelector } from '@tanstack/react-store'
|
|
265
|
+
|
|
266
|
+
function SelectedCount({ table }) {
|
|
267
|
+
const selection = useSelector(table.atoms.rowSelection)
|
|
268
|
+
return <span>{Object.keys(selection).length} selected</span>
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`<table.Subscribe>` still selects from `table.state` (the full state). For a single slice, `useSelector(table.atoms.X)` skips even constructing the snapshot.
|
|
273
|
+
Source: `docs/framework/react/guide/table-state.md`.
|
|
274
|
+
|
|
275
|
+
### MEDIUM Hoisting heavy table state reads above virtualizers
|
|
276
|
+
|
|
277
|
+
Wrong:
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
function App() {
|
|
281
|
+
const rowVirtualizer = useVirtualizer({
|
|
282
|
+
/* … */
|
|
283
|
+
}) // virtualizer too high
|
|
284
|
+
const table = useTable(opts)
|
|
285
|
+
return <TableBody table={table} virtualizer={rowVirtualizer} />
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Correct:
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
function App() {
|
|
293
|
+
const tableContainerRef = React.useRef<HTMLDivElement>(null)
|
|
294
|
+
const table = useTable(opts)
|
|
295
|
+
return (
|
|
296
|
+
<div ref={tableContainerRef} style={{ overflow: 'auto', height: 800 }}>
|
|
297
|
+
<TableBody table={table} tableContainerRef={tableContainerRef} />
|
|
298
|
+
</div>
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
function TableBody({ table, tableContainerRef }) {
|
|
302
|
+
const rowVirtualizer = useVirtualizer({
|
|
303
|
+
/* … */
|
|
304
|
+
}) // virtualizer at the bottom
|
|
305
|
+
/* … */
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The virtualizer in the deepest component avoids re-running on unrelated state changes.
|
|
310
|
+
Source: `examples/react/virtualized-rows/src/main.tsx`.
|
|
311
|
+
|
|
312
|
+
### MEDIUM Premature `<Subscribe>` / narrow selectors on small tables
|
|
313
|
+
|
|
314
|
+
Wrong:
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
// 50-row table with Subscribe around every cell.
|
|
318
|
+
header: ({ table }) => (
|
|
319
|
+
<Subscribe source={table.store} selector={(s) => s.sorting}>
|
|
320
|
+
{() => <SortHeader />}
|
|
321
|
+
</Subscribe>
|
|
322
|
+
)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Correct:
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
const table = useTable({ features, rowModels, columns, data })
|
|
329
|
+
// Reach for Subscribe later, scoped to actual hotspots.
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Advanced state-management patterns are for advanced cases. On small tables the boundary churn costs more than it saves.
|
|
333
|
+
Source: maintainer guidance (Phase 4).
|
|
334
|
+
|
|
335
|
+
## See Also
|
|
336
|
+
|
|
337
|
+
- `tanstack-table/react/table-state` — the API surface this skill optimizes against.
|
|
338
|
+
- `tanstack-table/react/react-subscribe-compiler-compat` — required reading if React Compiler is on.
|
|
339
|
+
- `tanstack-table/react/compose-with-tanstack-store` — fine-grained subscriptions via external atoms.
|
|
340
|
+
- `tanstack-table/react/compose-with-tanstack-virtual` — row/column virtualization patterns.
|
|
341
|
+
- `tanstack-table/react/compose-with-tanstack-devtools` — `/production` import for live devtools in prod.
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react/react-subscribe-compiler-compat
|
|
3
|
+
description: >
|
|
4
|
+
React Compiler compatibility for `@tanstack/react-table` v9. When you read
|
|
5
|
+
table state via builder APIs (`column.getIsPinned()`, `row.getIsSelected()`,
|
|
6
|
+
`cell.getIsAggregated()`, `header.column.getIsSorted()`) inside a nested
|
|
7
|
+
custom component, React Compiler memoizes the child against the stable
|
|
8
|
+
`column` / `row` / `cell` reference and never re-runs when the underlying
|
|
9
|
+
atom changes. Symptom: stale checkboxes, frozen sort indicators, dead pin
|
|
10
|
+
buttons. Fix: wrap the JSX in `<Subscribe source={table.store} selector={…}>`
|
|
11
|
+
or `<Subscribe source={table.atoms.X}>` so the dependency is visible to the
|
|
12
|
+
compiler. Routing keywords: Subscribe, table.Subscribe, React Compiler,
|
|
13
|
+
stale checkbox, memoized header/cell, builder API.
|
|
14
|
+
type: framework
|
|
15
|
+
library: tanstack-table
|
|
16
|
+
framework: react
|
|
17
|
+
library_version: '9.0.0-alpha.48'
|
|
18
|
+
requires:
|
|
19
|
+
- react/table-state
|
|
20
|
+
sources:
|
|
21
|
+
- TanStack/table:docs/framework/react/guide/table-state.md
|
|
22
|
+
- TanStack/table:packages/react-table/src/Subscribe.ts
|
|
23
|
+
- TanStack/table:examples/react/basic-subscribe/src/main.tsx
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
This skill builds on `tanstack-table/state-management` and `tanstack-table/react/table-state`. Read those first — the atom model is what makes builder reads invisible to React Compiler, and `table-state` covers the basics of `<Subscribe>` / `<table.Subscribe>`. This skill is laser-focused on the **one** React-specific failure mode that comes up when React Compiler is enabled.
|
|
27
|
+
|
|
28
|
+
## Why this exists
|
|
29
|
+
|
|
30
|
+
Under React Compiler, JSX is memoized against the props your component receives. A custom `DraggableHeader({ header })` receives a stable `header` reference; the compiler hashes the JSX it produces against that reference. When you call `header.column.getIsPinned()` inside that component, the compiler **cannot see the atom read** hidden behind the method — it returns the cached JSX, and the UI goes stale.
|
|
31
|
+
|
|
32
|
+
The fix is to make the dependency visible: read the slice via `<Subscribe>` or `<table.Subscribe>`. The compiler sees the selector function, picks up the dependency edge, and re-runs the children whenever the subscribed slice changes.
|
|
33
|
+
|
|
34
|
+
## Setup
|
|
35
|
+
|
|
36
|
+
You only need `<Subscribe>` from `@tanstack/react-table`. It's the same component shown in `table-state`, applied specifically around builder-pattern reads in custom nested components.
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { Subscribe } from '@tanstack/react-table'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Core Pattern: wrap nested builder reads in `<Subscribe>`
|
|
43
|
+
|
|
44
|
+
Whenever a child component reads state via a builder method (`getIs*`, `getCan*`, etc.) inside JSX that the compiler memoizes, wrap it in `<Subscribe>` keyed on the relevant slice.
|
|
45
|
+
|
|
46
|
+
### Pin / sort indicator on a custom header component
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { Subscribe } from '@tanstack/react-table'
|
|
50
|
+
|
|
51
|
+
function DraggableHeader({ header, table }) {
|
|
52
|
+
return (
|
|
53
|
+
<Subscribe
|
|
54
|
+
source={table.store}
|
|
55
|
+
selector={(s) => ({ columnPinning: s.columnPinning, sorting: s.sorting })}
|
|
56
|
+
>
|
|
57
|
+
{() => {
|
|
58
|
+
// Reads run inside the Subscribe child — re-evaluated on selected slice changes.
|
|
59
|
+
const isPinned = header.column.getIsPinned()
|
|
60
|
+
const sortDir = header.column.getIsSorted()
|
|
61
|
+
return (
|
|
62
|
+
<th data-pinned={isPinned}>
|
|
63
|
+
{header.column.id}
|
|
64
|
+
{sortDir === 'asc' ? ' 🔼' : sortDir === 'desc' ? ' 🔽' : null}
|
|
65
|
+
</th>
|
|
66
|
+
)
|
|
67
|
+
}}
|
|
68
|
+
</Subscribe>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Source: `docs/framework/react/guide/table-state.md` (Subscribe for React Compiler Compatibility); `packages/react-table/src/Subscribe.ts`.
|
|
74
|
+
|
|
75
|
+
### Row-selection checkbox inside a cell — narrowest subscription
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
columnHelper.display({
|
|
79
|
+
id: 'select',
|
|
80
|
+
cell: ({ row, table }) => (
|
|
81
|
+
// Subscribe to the rowSelection ATOM (not table.store) and project to ONE row.
|
|
82
|
+
// Re-renders ONLY when this row's selection flips.
|
|
83
|
+
<Subscribe
|
|
84
|
+
source={table.atoms.rowSelection}
|
|
85
|
+
selector={(rowSelection) => rowSelection[row.id]}
|
|
86
|
+
>
|
|
87
|
+
{(isSelected) => (
|
|
88
|
+
<input
|
|
89
|
+
type="checkbox"
|
|
90
|
+
checked={!!isSelected}
|
|
91
|
+
disabled={!row.getCanSelect()}
|
|
92
|
+
indeterminate={row.getIsSomeSelected()}
|
|
93
|
+
onChange={row.getToggleSelectedHandler()}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
</Subscribe>
|
|
97
|
+
),
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Source: `examples/react/basic-subscribe/src/main.tsx` (this exact pattern).
|
|
102
|
+
|
|
103
|
+
### Component-level vs cell-level — which API
|
|
104
|
+
|
|
105
|
+
| Context | `table` is | API |
|
|
106
|
+
| ------------------------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------------- |
|
|
107
|
+
| Component receiving `table` from `useTable` | `ReactTable<…>` | `<table.Subscribe>` works |
|
|
108
|
+
| Inside `cell: ({ table }) => …` / `header: ({ table }) => …` | core `Table<TFeatures, TData>` | `table.Subscribe` is **undefined**. Use the standalone `<Subscribe>` import. |
|
|
109
|
+
|
|
110
|
+
## Common Mistakes
|
|
111
|
+
|
|
112
|
+
### CRITICAL Builder read in a nested component without `<Subscribe>`
|
|
113
|
+
|
|
114
|
+
Wrong:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
function DraggableHeader({ header }) {
|
|
118
|
+
const isPinned = header.column.getIsPinned() // hidden atom read
|
|
119
|
+
return <th data-pinned={isPinned}>{header.column.id}</th>
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Correct:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import { Subscribe } from '@tanstack/react-table'
|
|
127
|
+
|
|
128
|
+
function DraggableHeader({ header, table }) {
|
|
129
|
+
return (
|
|
130
|
+
<Subscribe source={table.store} selector={(s) => s.columnPinning}>
|
|
131
|
+
{() => {
|
|
132
|
+
const isPinned = header.column.getIsPinned()
|
|
133
|
+
return <th data-pinned={isPinned}>{header.column.id}</th>
|
|
134
|
+
}}
|
|
135
|
+
</Subscribe>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
React Compiler memoizes the child's JSX against the stable `header` reference. The state-dependent builder method hides its atom dependency, so the memoized JSX never re-runs.
|
|
141
|
+
Source: `docs/framework/react/guide/table-state.md`; `examples/react/basic-subscribe/src/main.tsx`; `packages/react-table/src/Subscribe.ts`.
|
|
142
|
+
|
|
143
|
+
### HIGH Using `table.Subscribe` from inside a cell or header definition
|
|
144
|
+
|
|
145
|
+
Wrong:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
cell: ({ row, table }) => (
|
|
149
|
+
<table.Subscribe
|
|
150
|
+
source={table.atoms.rowSelection}
|
|
151
|
+
selector={(s) => s[row.id]}
|
|
152
|
+
>
|
|
153
|
+
{(isSelected) => <input type="checkbox" checked={!!isSelected} />}
|
|
154
|
+
</table.Subscribe>
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Correct:
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
import { Subscribe } from '@tanstack/react-table'
|
|
162
|
+
|
|
163
|
+
cell: ({ row, table }) => (
|
|
164
|
+
<Subscribe source={table.atoms.rowSelection} selector={(s) => s[row.id]}>
|
|
165
|
+
{(isSelected) => (
|
|
166
|
+
<input
|
|
167
|
+
type="checkbox"
|
|
168
|
+
checked={!!isSelected}
|
|
169
|
+
onChange={row.getToggleSelectedHandler()}
|
|
170
|
+
/>
|
|
171
|
+
)}
|
|
172
|
+
</Subscribe>
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Cell and header render contexts type `table` as `Table<TFeatures, TData>`, not `ReactTable` — `table.Subscribe` is undefined. Import the standalone `<Subscribe>`.
|
|
177
|
+
Source: `docs/framework/react/guide/table-state.md` (Tips); `packages/react-table/src/Subscribe.ts`.
|
|
178
|
+
|
|
179
|
+
### MEDIUM Wrapping every cell in `<Subscribe>` by default
|
|
180
|
+
|
|
181
|
+
Wrong:
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
// Inline cell that already re-runs on every parent render — wrap is unnecessary.
|
|
185
|
+
{
|
|
186
|
+
row.getVisibleCells().map((cell) => (
|
|
187
|
+
<Subscribe source={table.store} selector={(s) => s.rowSelection}>
|
|
188
|
+
{() => (
|
|
189
|
+
<td>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
|
|
190
|
+
)}
|
|
191
|
+
</Subscribe>
|
|
192
|
+
))
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Correct:
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
{
|
|
200
|
+
row.getVisibleCells().map((cell) => (
|
|
201
|
+
<td key={cell.id}>
|
|
202
|
+
<table.FlexRender cell={cell} />
|
|
203
|
+
</td>
|
|
204
|
+
))
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`<Subscribe>` is overhead. For inline JSX in the parent component the compiler always re-evaluates on parent re-render, so wrapping adds subscription churn without correctness benefit. Reach for `<Subscribe>` only at custom-component boundaries that the compiler memoizes.
|
|
209
|
+
Source: `docs/framework/react/guide/table-state.md`.
|
|
210
|
+
|
|
211
|
+
### MEDIUM Subscribing to the whole `table.store` for one row's checkbox
|
|
212
|
+
|
|
213
|
+
Wrong:
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
<Subscribe source={table.store} selector={(s) => s.rowSelection[row.id]}>
|
|
217
|
+
{(isSelected) => <input type="checkbox" checked={!!isSelected} />}
|
|
218
|
+
</Subscribe>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Correct:
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
<Subscribe source={table.atoms.rowSelection} selector={(s) => s[row.id]}>
|
|
225
|
+
{(isSelected) => (
|
|
226
|
+
<input
|
|
227
|
+
type="checkbox"
|
|
228
|
+
checked={!!isSelected}
|
|
229
|
+
onChange={row.getToggleSelectedHandler()}
|
|
230
|
+
/>
|
|
231
|
+
)}
|
|
232
|
+
</Subscribe>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Every change to `table.store` re-runs the Subscribe child. Subscribing to `table.atoms.rowSelection` (a single slice atom) with a per-row projection limits work to actual selection changes for that row.
|
|
236
|
+
Source: `examples/react/basic-subscribe/src/main.tsx`; `docs/framework/react/guide/table-state.md` (Tips).
|
|
237
|
+
|
|
238
|
+
### CRITICAL Reading raw state with `table.getState()` on v9 instead of `<Subscribe>`
|
|
239
|
+
|
|
240
|
+
Wrong:
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
function Toolbar({ table }) {
|
|
244
|
+
// v8 muscle memory — does NOT subscribe in v9.
|
|
245
|
+
const { rowSelection } = table.getState()
|
|
246
|
+
return <div>{Object.keys(rowSelection).length} selected</div>
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Correct:
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
function Toolbar({ table }) {
|
|
254
|
+
return (
|
|
255
|
+
<table.Subscribe selector={(s) => Object.keys(s.rowSelection).length}>
|
|
256
|
+
{(count) => <div>{count} selected</div>}
|
|
257
|
+
</table.Subscribe>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`table.getState()` is a current-value read in v9; it does not subscribe the component. The default `useTable` selector subscribes the parent, but deeply-nested children should opt in explicitly.
|
|
263
|
+
Source: PR #6246; `packages/react-table/src/useTable.ts` JSDoc.
|
|
264
|
+
|
|
265
|
+
## See Also
|
|
266
|
+
|
|
267
|
+
- `tanstack-table/react/table-state` — base `<Subscribe>` / `<table.Subscribe>` API and external atoms.
|
|
268
|
+
- `tanstack-table/react/production-readiness` — selector narrowing and per-slice `useSelector(table.atoms.X)`.
|
|
269
|
+
- `tanstack-table/react/migrate-v8-to-v9` — replacing `useReactTable` with `useTable` to fix the React Compiler "incompatible library" warning.
|