pyreon

@pyreon/table — API Reference

Generated from table's src/manifest.ts — the same source that powers llms.txt and MCP get_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

SymbolKindSummary
useTablehookCreate a reactive TanStack Table v9 instance.
flexRenderfunctionRender a TanStack Table column definition template (header, cell, or footer).
flexRenderCellfunctionFine-grained per-cell renderer for live cell values.
visibleCellsfunctionFine-grained visible-cells accessor for a row — the cells-LIST companion to flexRenderCell, for the inner <For> of a
createTableStatefunctionThe 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 v9 useTable returns the Table INSTANCE, not a Computed. The v8 accessor call is gone; reads track natively

  • Forgetting to register a feature — v9 exposes an API only when its feature is in tableFeatures({...}). If table.nextPage or column.toggleSorting is missing, add rowPaginationFeature / rowSortingFeature (plus its row-model slot); do NOT cast the table to a broader type

  • Building the features object inside the component or inline in the options function — it is a compile-time type parameter, so define it once at module scope

  • Using .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 via style={() => ({ width: table.getColumn(id).getSize() + "px" })}

See also: flexRender · flexRenderCell


flexRender function

<TValue>(component: Renderable<TValue>, props: TValue) => unknown

Render 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 captured cell is stale and the reused row never re-runs it, so it freezes. Use flexRenderCell(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) => unknown

Fine-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 change

  • Passing a table built directly with constructTable instead of one from useTable — 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 captured row instead — 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 data as 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: setFilter resets to page 0, but deleting rows or a refetch that returns fewer does not. page() is clamped on READ, so it and rows() and pageCount() 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, plus tableFeatures/stockFeatures. All types are re-exported too. Import from @pyreon/table, not @tanstack/table-core. The runtime list is explicit and curated (not export *) 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 with visibleCells(table, row.id) — NOT the captured row.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 flexRender cells too, not just flexRenderCell). 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.

TanStack Table Adapter — API Reference