preact-production-readiness

star 28.1k

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.

TanStack By TanStack schedule Updated 6/5/2026

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.

// Bad — pulls every feature into the bundle even if the UI never uses them.
const features = tableFeatures({
  rowPaginationFeature,
  rowSortingFeature,
  rowSelectionFeature,
  columnFilteringFeature,
  globalFilteringFeature,
  columnFacetingFeature,
  globalFacetingFeature,
  columnGroupingFeature,
  rowExpandingFeature,
  columnSizingFeature,
  columnVisibilityFeature,
  columnOrderingFeature,
  columnPinningFeature,
  rowPinningFeature,
})

// Good — feature list matches what the UI exposes.
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.

// Best — module scope. Single allocation.
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 })
}

// Okay — useMemo for dynamic columns.
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.

// All slices — fine for a small table.
const table = useTable(opts, (state) => state)

// Narrow — re-render only on sorting/pagination changes.
const table = useTable(opts, (state) => ({
  sorting: state.sorting,
  pagination: state.pagination,
}))
table.state.pagination

// Opt-out at the parent; do subscriptions lower in the tree.
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)

// Row body — re-render only when filters/pagination cause the row model to change.
<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>

// Per-row selection checkbox — narrow to that row's selection bit.
<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.

// Less granular — every slice change re-renders this component.
const [sorting,    setSorting]    = useState<SortingState>([])
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 10 })
useTable({
  /* … */,
  state: { sorting, pagination },
  onSortingChange: setSorting,
  onPaginationChange: setPagination,
})

// More granular — independent atom subscriptions.
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 }) // new object every render
  useTable({ features, columns, data })
}

Correct:

const features = tableFeatures({ rowSortingFeature }) // module scope

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).
Install via CLI
npx skills add https://github.com/TanStack/table --skill preact-production-readiness
Repository Details
star Stars 28,079
call_split Forks 3,522
navigation Branch main
article Path SKILL.md
More from Creator