@pyreon/table — API Reference
Generated from
table'ssrc/manifest.ts— the same source that powersllms.txtand MCPget_api. Do not edit this page by hand; edit the manifest. For the conceptual guide, see table.
Reactive TanStack Table v9 adapter for Pyreon. Options are passed as a function so signal reads inside (data, columns, state) automatically re-sync the table when any tracked signal changes. Returns the Table instance directly: its state lives in Pyreon signals through v9's pluggable reactivity seam, so reads track natively inside templates and effects. Re-exports the TanStack Table author surface — all 16 features, every row model and built-in fn — as an explicit, curated list.
Features
useTable(optionsFn) with reactive signal-driven options
flexRender for column def templates (strings, functions, VNodes)
flexRenderCell — fine-grained per-cell updates: an in-place data edit patches only the changed rows cells, no memo boilerplate
Full TanStack Table core re-exported — single import source
Pyreon signals ARE the table's reactive atoms (v9 coreReactivityFeature bindings) — no version counter, no accessor wrapper
Complete example
A full, end-to-end usage of the package:
import {
useTable, flexRender, flexRenderCell, visibleCells,
tableFeatures, rowSortingFeature, createSortedRowModel, sortFn_alphanumeric,
type ColumnDef,
} from '@pyreon/table'
import { signal } from '@pyreon/reactivity'
interface User { name: string; email: string; age: number }
// v9 registers capabilities EXPLICITLY — define the set once, at module scope,
// with only what this table uses (that is what keeps the bundle small).
const features = tableFeatures({
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
sortFns: { alphanumeric: sortFn_alphanumeric },
})
const users = signal<User[]>([
{ name: 'Alice', email: 'alice@example.com', age: 30 },
{ name: 'Bob', email: 'bob@example.com', age: 25 },
])
const columns: ColumnDef<typeof features, User>[] = [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
{ accessorKey: 'age', header: 'Age' },
]
// Options as a FUNCTION — signal reads inside auto-track.
// Changing users() re-syncs the entire table reactively.
const table = useTable(() => ({
features,
data: users(),
columns,
}))
// In JSX — read the table inside reactive scopes (no accessor call):
<table>
<thead>
<For each={() => table.getHeaderGroups()} by={(g) => g.id}>
{(group) => (
<tr>
<For each={() => group.headers} by={(h) => h.id}>
{(header) => (
<th onClick={header.column.getToggleSortingHandler()}>
{flexRender(header.column.columnDef.header, header.getContext())}
</th>
)}
</For>
</tr>
)}
</For>
</thead>
<tbody>
<For each={() => table.getRowModel().rows} by={(r) => r.id}>
{(row) => (
<tr>
<For each={() => visibleCells(table, row.id)} by={(c) => c.id}>
{/* flexRenderCell(table, …) inside an accessor = fine-grained:
a single-cell edit patches ONLY this cell. Plain
flexRender(cell…, cell.getContext()) FREEZES on a value change
because the keyed <For> reuses the cell and never re-runs it. */}
{(cell) => <td>{() => flexRenderCell(table, row.id, cell.column.id)}</td>}
</For>
</tr>
)}
</For>
</tbody>
</table>Exports
| Symbol | Kind | Summary |
|---|---|---|
useTable | hook | Create a reactive TanStack Table v9 instance. |
flexRender | function | Render a TanStack Table column definition template (header, cell, or footer). |
flexRenderCell | function | Fine-grained per-cell renderer for live cell values. |
visibleCells | function | Fine-grained visible-cells accessor for a row — the cells-LIST companion to flexRenderCell, for the inner <For> of a |
createTableState | function | The dependency-free, MULTIPLATFORM-portable table-state core — the alternative to useTable (which binds `@tanstack/tab |
API
useTable hook
<TFeatures extends TableFeatures, TData extends RowData>(options: () => TableOptions<TFeatures, TData>) => Table<TFeatures, TData>Create a reactive TanStack Table v9 instance. Options are passed as a function so reactive signals (data, columns, state) can be read inside and the table re-syncs automatically when they change. Returns the Table instance DIRECTLY — its state lives in Pyreon signals via v9's coreReactivityFeature seam, so reading it inside any reactive scope (a JSX accessor, an effect, a computed) subscribes natively. v9 requires every non-core capability to be registered explicitly in a features object built with tableFeatures({...}); the core row model is automatic.
Example
// Define the feature set ONCE, outside the component — only what you use.
const features = tableFeatures({
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
sortFns: { alphanumeric: sortFn_alphanumeric },
})
const table = useTable(() => ({
features,
data: users(),
columns: [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
],
}))
// Read inside a reactive scope — no accessor call, the table IS the instance:
<For each={() => table.getRowModel().rows} by={(r) => r.id}>
{(row) => <tr>...</tr>}
</For>Common mistakes
Passing options as a plain object instead of a function — signal reads are not tracked and the table never updates when data changes
Calling
table()— under v9useTablereturns the Table INSTANCE, not a Computed. The v8 accessor call is gone; reads track nativelyForgetting to register a feature — v9 exposes an API only when its feature is in
tableFeatures({...}). Iftable.nextPageorcolumn.toggleSortingis missing, addrowPaginationFeature/rowSortingFeature(plus its row-model slot); do NOT cast the table to a broader typeBuilding the
featuresobject inside the component or inline in the options function — it is a compile-time type parameter, so define it once at module scopeUsing
.map()on rows instead of<For>— loses Pyreon's keyed reconciliation, rebuilds the whole tbody on every change (worst-case DOM churn)Binding a value that CHANGES (a cell value, column width from
getSize(), a sort indicator) as a STATIC prop/attr/child through a keyed<For>— the keyed cell is reused on a state change and its body never re-runs, so the value freezes. Read it inside a reactive closure at the point of use: cell content via<td>{() => flexRenderCell(table, row.id, cell.column.id)}</td>, an attribute viastyle={() => ({ width: table.getColumn(id).getSize() + "px" })}
See also: flexRender · flexRenderCell
flexRender function
<TValue>(component: Renderable<TValue>, props: TValue) => unknownRender a TanStack Table column definition template (header, cell, or footer). Handles strings, numbers, functions (component functions or render functions), and VNodes. Returns the rendered output or null for undefined/null inputs. Use in JSX to render column definitions provided by TanStack Table.
Example
// Header:
flexRender(header.column.columnDef.header, header.getContext())
// Cell:
flexRender(cell.column.columnDef.cell, cell.getContext())Common mistakes
Wrapping flexRender output in an extra function accessor — the result is already renderable JSX content
Passing the column def directly instead of calling getContext() — TanStack Table requires the context object
Using plain
flexRender(cell…, cell.getContext())for a cell inside a keyed<For>when the cell VALUE can change in place — the capturedcellis stale and the reused row never re-runs it, so it freezes. UseflexRenderCell(table, row.id, cell.column.id)for live cells.
See also: useTable · flexRenderCell
flexRenderCell function
<TFeatures extends TableFeatures, TData extends RowData>(table: Table<TFeatures, TData>, rowId: string, columnId: string) => unknownFine-grained per-cell renderer for live cell values. Inside a keyed <For>, the row/cell objects are captured ONCE (the reconciler reuses the DOM node and never re-runs its body), so plain flexRender(cell…, cell.getContext()) FREEZES when a value changes in place. flexRenderCell re-navigates to the live cell from the current row model each read — place it in an explicit accessor <td>{() => flexRenderCell(table, row.id, cell.column.id)}</td>. A table from useTable carries a per-row signal bridge, so the cell subscribes to ONLY its own row's signal and an in-place data edit patches just the changed rows' cells — matching a hand-memoized react-table row without any React.memo boilerplate. Returns null when the row is not in the current (filtered/paginated) row model.
Example
// Place inside an accessor child so a single-cell edit patches ONLY that cell:
// <td>{() => flexRenderCell(table, row.id, cell.column.id)}</td>
flexRenderCell(table, row.id, columnId)Common mistakes
Forgetting the explicit accessor wrapper
{() => …}— without it the cell is captured once and freezes on the next changePassing a table built directly with
constructTableinstead of one fromuseTable— it renders correctly but has no per-row bridge, so it subscribes coarsely (every cell re-runs on any change)
See also: useTable · flexRender
visibleCells function
<TFeatures extends TableFeatures, TData extends RowData>(table: Table<TFeatures, TData>, rowId: string) => Cell[]Fine-grained visible-cells accessor for a row — the cells-LIST companion to flexRenderCell, for the inner <For> of a keyed table body: <For each={() => visibleCells(table, row.id)} by={(c) => c.id}>. The naive each={() => row.getVisibleCells()} leaves a TRACKED table-core read in every row's scope (its memo deps read table.options, which changes on EVERY options sync — data edits included), so a single-cell edit re-ran every row's cells-list accessor: measured 1000 re-runs at N=1000 where 1 is correct, ~3× the wall-clock of a memoized react-table update. visibleCells subscribes to the row's own signal plus the column-geometry state slices (visibility, order, pinning, grouping) and looks the cells up UNTRACKED from the CURRENT row model — never a captured stale row — so a data edit reaches exactly the edited rows' loops while a real visibility/order/pinning change still re-reconciles every row's cell list. Falls back to tracked (coarse but correct) reads for a table built directly with constructTable (no bridge). Returns an empty array when the row is not in the current model.
Example
<For each={() => table.getRowModel().rows} by={(r) => r.id}>
{(row) => (
<tr>
<For each={() => visibleCells(table, row.id)} by={(c) => c.id}>
{(cell) => <td>{() => flexRenderCell(table, row.id, cell.column.id)}</td>}
</For>
</tr>
)}
</For>Common mistakes
Using
each={() => row.getVisibleCells()}on the capturedrowinstead — it works, but subscribes every row to the options atom, so every data edit re-runs ALL N cells-list accessors (the O(N)-per-edit overhead that made single-cell updates ~3× slower than memoized react-table at N=1000)Calling it OUTSIDE a reactive scope — like any accessor it must be read where tracking is live (a
<For each>accessor, an effect) or it never re-runs
See also: flexRenderCell · useTable
createTableState function
<T>(options: { data: () => readonly T[]; columns?: TableColumn<T>[]; pageSize?: number; rowId?: (row: T, i: number) => string; filterFn?: (row: T, q: string, cols: TableColumn<T>[]) => boolean }) => TableState<T>The dependency-free, MULTIPLATFORM-portable table-state core — the alternative to useTable (which binds @tanstack/table-core and is web-only-rich). Pure signal logic (no DOM, no TanStack), so the SAME source drives sort / filter / paginate / row-selection on web AND, via co-located native Swift/Kotlin ports (PyreonTableState, behaviour-identical + compile-and-run verified), on iOS/Android — you render rows() with native <For> (tables ARE native: SwiftUI List / Compose LazyColumn), no WebView. data is an ACCESSOR so a signal()/computed() source stays reactive; rows() re-derives filtered → sorted → paginated. toggleSort cycles none → asc → desc → none; the filter is case-insensitive across every column (override with filterFn); pageSize: 0 disables pagination. page() is CLAMPED against the live row count, so a page that falls off the end when the data shrinks reports the last page rather than rendering a blank table, and a transient shrink returns the reader to where they were. Empty cells (null/undefined) sort as ONE rank, so rows with nothing in the sorted column keep their relative order. A createTableState-only import tree-shakes TanStack out entirely.
Example
const data = signal([{ id: 1, name: 'Ada' }, { id: 2, name: 'Linus' }])
const table = createTableState({ data: () => data(), columns: [{ id: 'name' }], pageSize: 10, rowId: (r) => String(r.id) })
table.toggleSort('name'); table.setFilter('li')
// <For each={table.rows()} by={(r) => r.id}>{(r) => <Text>{r.name}</Text>}</For>Common mistakes
Passing
dataas a plain array instead of an accessor() => data()— the table then never re-derives when the source signal changes.Expecting a data shrink to reset the page:
setFilterresets to page 0, but deleting rows or a refetch that returns fewer does not.page()is clamped on READ, so it androws()andpageCount()always agree — but the RAW page you last set is remembered, which is why a transient shrink returns you to it.Reaching for it when you need grouping / faceting / column pinning / virtual sizing — those stay on the full
useTable(TanStack) web path; this is the common 80% (sort/filter/paginate/select).
See also: useTable
Package-level notes
Note: Options must be a FUNCTION
() => TableOptions<T>, not a plain object. Signal reads inside the function are tracked reactively — changing any tracked signal re-syncs the table automatically.
Re-exports: The TanStack Table author surface is re-exported from
@pyreon/table— all 16 features (rowSortingFeature, columnFilteringFeature, …), every row model (createSortedRowModel, createFilteredRowModel, createPaginatedRowModel, …), every built-in filter/sort/aggregation fn, plustableFeatures/stockFeatures. All types are re-exported too. Import from@pyreon/table, not@tanstack/table-core. The runtime list is explicit and curated (notexport *) so an upstream major is OUR migration, not yours, and adapter-construction internals never leak.
Computed return: useTable returns the Table INSTANCE (v9), not a Computed — there is no
table()call. Its state lives in Pyreon signals, so reading it inside a reactive scope subscribes:<For each={() => table.getRowModel().rows}>makes the list reactive. The v8 accessor form was only ever a workaround for v8 having no reactivity seam.
Fine-grained cells: For live/editable tables, render cells with
flexRenderCell(table, row.id, cell.column.id)inside an accessor, and drive the inner cells loop withvisibleCells(table, row.id)— NOT the capturedrow.getVisibleCells(), whose tracked memo-dep reads subscribe every row to the options atom (a data edit then re-runs ALL N cells-list accessors; measured ~3× a memoized react-table update at N=1000). With both, an in-place data edit re-runs ONLY the changed rows' bindings and patches ONE cell — no memo boilerplate, matching (and on wall-clock beating) a hand-optimized react-table. A table-STATE change (sort/filter/selection/column visibility) re-runs all cells (coarse, correct-by-default for state-reading cells).
reorder-on-data-edit limitation: A DATA edit that changes the SORT ORDER (editing the column you are sorted BY) updates every cell to the correct value but does NOT re-position the keyed rows until the next structure/state change — a pre-existing base-adapter limitation of the sorted-row-model + <For> interaction (it affects plain
flexRendercells too, not justflexRenderCell). Re-ordering via the sort controls (toggleSorting/setSorting) works normally. Workaround: re-apply sorting after such an edit, or sort by a column you do not edit in place.