| name | preact/production-readiness |
| description | Ship-ready optimizations for `@tanstack/preact-table` v9: tree-shake the bundle by registering ONLY the `features` you actually use; memoize `features`, `data`, and `columns` for stable identity; replace `(state) => state` with narrow selectors or per-slice `useSelector` subscriptions; wrap hot subtrees in `<table.Subscribe>`; and prefer slice atoms over `state` + `on*Change` for fine-grained updates. Routing keywords: preact-table performance, optimization, tree-shaking, stable refs, Subscribe, narrow selector.
|
| type | lifecycle |
| library | tanstack-table |
| framework | preact |
| library_version | 9.0.0-alpha.48 |
| requires | ["setup","state-management","preact/table-state"] |
| sources | ["TanStack/table:docs/guide/features.md","TanStack/table:docs/framework/preact/guide/table-state.md","TanStack/table:examples/preact/basic-subscribe/src/main.tsx","TanStack/table:examples/preact/basic-external-atoms/src/main.tsx","TanStack/table:packages/preact-table/src/useTable.ts"] |
This skill collects the production-readiness levers for a Preact v9 table. Each one is independent — apply only the ones whose problem you actually have.
1. Tree-Shake features
Only register features the table actually uses. v9's bundle savings come from features controlling which feature code (and which state slices and APIs) get included.
const features = tableFeatures({
rowPaginationFeature,
rowSortingFeature,
rowSelectionFeature,
columnFilteringFeature,
globalFilteringFeature,
columnFacetingFeature,
globalFacetingFeature,
columnGroupingFeature,
rowExpandingFeature,
columnSizingFeature,
columnVisibilityFeature,
columnOrderingFeature,
columnPinningFeature,
rowPinningFeature,
})
const features = tableFeatures({
rowPaginationFeature,
rowSortingFeature,
rowSelectionFeature,
})
The same idea applies to row-model factories: only register factories for features that need one and that you have registered, by including them in the same tableFeatures({...}) call.
Source: docs/guide/features.md; docs/framework/preact/preact-table.md.
2. Stable References for features, columns, and data
Identity drives every internal memo. Declare these at module scope when possible; otherwise wrap with useMemo.
const features = tableFeatures({ rowSortingFeature })
const columns: Array<ColumnDef<typeof features, Person>> = [
]
const EMPTY: Person[] = []
function MyTable({ rows }: { rows: Person[] | undefined }) {
const data = rows ?? EMPTY
const table = useTable({ features, columns, data })
}
function MyTable({ visibleKeys }: { visibleKeys: string[] }) {
const columns = useMemo(
() => visibleKeys.map((k) => columnHelper.accessor(k as any, {})),
[visibleKeys.join(',')],
)
}
Source: docs/framework/preact/guide/table-state.md (FAQ #1).
3. Narrow useTable Selector
The default selector (state) => state re-renders the component on any registered slice change. Narrow it to just the slices the component reads. The Preact adapter uses shallow compare from @tanstack/preact-store — projected objects only trigger a render when a member changes.
const table = useTable(opts, (state) => state)
const table = useTable(opts, (state) => ({
sorting: state.sorting,
pagination: state.pagination,
}))
table.state.pagination
const table = useTable(opts, () => null)
Source: examples/preact/basic-subscribe/src/main.tsx.
4. Wrap Hot Subtrees in <table.Subscribe>
Once the parent uses () => null, push subscriptions next to the UI that actually reads them. Subscribe to single atoms (source={table.atoms.X}) to avoid re-deriving the flat store on unrelated changes.
const table = useTable(opts, () => null)
<table.Subscribe
selector={(s) => ({
columnFilters: s.columnFilters,
globalFilter: s.globalFilter,
pagination: s.pagination,
})}
>
{() => (
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>{/* … */}</tr>
))}
</tbody>
)}
</table.Subscribe>
<table.Subscribe
source={table.atoms.rowSelection}
selector={(rs) => rs[row.id]}
>
{(isSelected) => (
<input
type="checkbox"
checked={!!isSelected}
onChange={row.getToggleSelectedHandler()}
/>
)}
</table.Subscribe>
Source: examples/preact/basic-subscribe/src/main.tsx.
5. Prefer Slice Atoms over state + on*Change
External state + on*Change re-renders the whole component that owns the useState. Slice atoms let useSelector / <table.Subscribe> subscribe individually.
const [sorting, setSorting] = useState<SortingState>([])
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 10 })
useTable({
,
state: { sorting, pagination },
onSortingChange: setSorting,
onPaginationChange: setPagination,
})
const sortingAtom = useCreateAtom<SortingState>([])
const paginationAtom = useCreateAtom<PaginationState>({ pageIndex: 0, pageSize: 10 })
useTable({ , atoms: { sorting: sortingAtom, pagination: paginationAtom } })
Source: examples/preact/basic-external-atoms/src/main.tsx.
6. Set Sensible initialState Once
Use initialState for starting values. Setting state in an effect after mount triggers an extra render.
const table = useTable({
features,
columns,
data,
initialState: {
pagination: { pageIndex: 0, pageSize: 25 },
sorting: [{ id: 'createdAt', desc: true }],
},
})
Source: docs/framework/preact/guide/table-state.md.
7. Reach for createTableHook for Multi-Table Apps
When several screens share the same features (including row model factories) and conventions, createTableHook centralizes the configuration and lets you ship pre-bound cell/header components. Tables collapse to columns + data.
Source: docs/framework/preact/guide/create-table-hook.md.
Common Mistakes
CRITICAL tableFeatures(...) inside the component body
Wrong:
function MyTable() {
const features = tableFeatures({ rowSortingFeature })
useTable({ features, columns, data })
}
Correct:
const features = tableFeatures({ rowSortingFeature })
function MyTable() {
useTable({ features, columns, data })
}
A new features reference each render busts every memo that keys off it.
Source: docs/framework/preact/guide/table-state.md (FAQ #1).
CRITICAL Reimplementing built-ins manually
Wrong:
const sorted = useMemo(() => [...data].sort(), [data, sorting])
Correct:
const features = tableFeatures({
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
sortFns,
})
const table = useTable({ features, columns, data })
v9 ships built-ins for sorting, filtering, pagination, grouping, expanding, faceting, row selection, column visibility/order/pinning/sizing, and row pinning. Hand-rolling these is the #1 AI tell.
Source: docs/guide/features.md.
HIGH () => state selector everywhere
Wrong: every component using useTable(opts, (state) => state) re-renders on any slice change. Fine for small tables; expensive for kitchen-sink screens.
Correct: pass a narrow selector or () => null at large tables, then <table.Subscribe> lower.
Source: examples/preact/basic-subscribe/src/main.tsx.
HIGH New atom per render
Wrong: createAtom(...) inside the component body.
Correct: useCreateAtom(...) (or atom at module scope).
Source: examples/preact/basic-external-atoms/src/main.tsx.
MEDIUM Subscribe everywhere on a small table
Wrong: a 50-row table with <Subscribe> wrapped around every cell. Adds complexity, no measurable win.
Correct: default selector + inline rendering. Reach for Subscribe after measuring a hotspot.
See Also
tanstack-table/preact/table-state — Subscribe / atoms reference.
tanstack-table/preact/migrate-v8-to-v9 — what to replace from v8.
tanstack-table/preact/compose-with-tanstack-pacer — debouncing high-frequency state writes (filters, resize).