Grid The PretableSurface component
The PretableSurface component
The full React grid surface with declarative and explicit row-model ownership.
<PretableSurface> is the production grid component: indexed rendering,
selection, keyboard navigation, editing, clipboard, filters, sorting, grouping,
native date and number formatting, and custom presentation hooks.
Declarative rows mode
A live-events grid in the default rows mode — pass rows and columns, and PretableSurface reconciles later rows props into one long-lived local row model for you.
That's the whole recipe — typed columns, rows, and ariaLabel:
import { createColumnHelper } from "@pretable/core";
import { PretableSurface } from "@pretable/react";
interface Event {
id: string;
timestamp: string;
message: string;
}
const column = createColumnHelper<Event>();
const columns = [
column.accessor("timestamp", { type: "text", header: "Time" }),
column.accessor("message", { type: "text", header: "Message" }),
] as const;
export function EventGrid({ events }: { events: Event[] }) {
return (
<PretableSurface
ariaLabel="Live events"
rows={events}
columns={columns}
viewportHeight={520}
/>
);
}The surface creates one local row model for its lifetime and reconciles later rows props by stable ID. This is the default application path. Rows with a string or number id need no identity prop; pass getRowId for another stable key.
Explicit-model mode
Use explicit ownership for streaming or other imperative producers. Below, a chat grid owns its row model directly and connects it to a stream with connectElementStream — the same explicit-model shape, applied to @pretable/stream-adapter's highest-frequency case:
Turn a streaming LLM response into rows with connectElementStream and append them to the grid as they arrive.
The shape in general:
const rowModel = createLocalRowModel({ rows: [], columns });
const connection = connectElementStream(rowModel, stream);
<PretableSurface
ariaLabel="Streaming events"
model={rowModel}
viewportHeight={520}
/>;The modes are mutually exclusive: do not pass rows or getRowId with model. Optional columns in model mode may change presentation, not accessors, comparators, filters, aggregates, or IDs.
Configuration
| Area | Props |
|---|---|
| Geometry | viewportHeight, viewportStyle, overscan |
| Rendering | renderBodyCell, renderHeaderCell, row/cell/header class and prop callbacks |
| Components | components — replace a kit component everywhere; see Components |
| Value presentation | locale; column dateFormat, numberFormat, format, and formatAggregate |
| Controlled UI | state, onSelectionChange, onFocusChange |
| Row selection | rowSelectionColumn, state.rowSelection, onRowSelectionChange — its own slice |
| Rows-mode query | the exact query, onQueryChange pair |
| Editing | rows mode: onRowChange; model mode: beforeRowChange; shared: onPaste |
| Clipboard | onCopy, copyToClipboard, copyWithHeaders, messages |
| Grouping | groupPanel, groupColumn, hideGroupedColumns, initialExpansion |
| Tool panel | toolPanel — on by default; see Tool panel |
| Observation | onGridReady, onTelemetryChange, onRejectedWriteChange |
Rows-mode edit callbacks are proposals: update your rows value and let the next prop reconcile. Explicit-model edits validate the batch and publish one row-model transaction.
Native dateFormat / numberFormat options and locale format compatible
cells, inherited group aggregates, built-in clipboard text, and CSV through one
precedence pipeline. format and formatAggregate remain the custom
overrides. Canonical date behavior is presentation-only here: raw values stay
YYYY-MM-DD | null, and external query authority is never reapplied locally.
See Date formatting and Number
formatting.
For common semantic presentations, PretableBadge, PretableDelta, PretableEntity, and PretableStatus provide theme-aware contrast, direction markers, primary/secondary text, and state dots without rebuilding cell behavior. See Cell presentations.
Controlled state and ownership
The rows-mode query/onQueryChange pair owns filters, sort, and grouping together. The state prop controls UI interaction and column-layout slices. In explicit-model mode, application code sends query and row mutations to rowModel; an onGridReady handle remains UI-only.
See Row grouping and aggregation for typed grouping, aggregate, expansion, and group-panel contracts.
Server-applied filtering and sorting
processing declares whether filter and sort work was performed by the local engine or an external authority; resultMeta then describes the result those rows came from, and dataState says where its request stands. Both are acted on in rows mode: the surface stops the engine applying query.filters and query.sort while continuing to publish them, so the funnel, onQueryChange, and aria-sort are unchanged. All of it — query ownership, totals, the lifecycle phases, and renderBodyState — is documented against a live endpoint in Server-side data.
Telemetry
Telemetry distinguishes source/logical rows from the currently rendered viewport range. Keep onTelemetryChange stable with useCallback; it may fire during hot rendering paths.
| Field | Type | Notes |
|---|---|---|
focusedRowId | TRowId | PretableGroupId | null | The focused row, the focused group row's id under grouping, or null when nothing is focused. |
loadedRowCount | number | Source rows loaded into the row model. |
renderedRowCount | number | Rows currently in the DOM — the viewport range plus overscan. |
rowModelRowCount | number | Visible rows the model holds after filtering, group rows included. |
selectedRowId | TRowId | null | Start row of the first selection range, or null. |
totalHeight | number | Scroll height of every row, in px. |
totalRowCount | number | Source rows before filtering — today the same number loadedRowCount reports. |
visibleRowCount | number | Rows intersecting the viewport, overscan excluded. |
visibleRowRange | { end: number; start: number } | Half-open row-index range of those rows; { start: 0, end: 0 } when none are visible. |
windowGap | { readonly direction: "before" | "after"; readonly rowCount: number } | Optional; the near-edge signal for windowed datasets. |
Rejected writes
An invalid rows, derivations, or query update is a rejected write: the
grid keeps its last-good value and stays alive rather than unmounting. The
console warning that accompanies a rejection latches per fault kind — it fires
once and stays quiet — so it is a debugging aid, not a signal your code can
act on. The programmatic signal is the rejected-writes record:
onRejectedWriteChange fires on every transition, including recovery, with a
per-kind record — { rows, derivations, query }, each null when that slot
is in sync or a PretableRejectedWrite (kind, code, message, and
columnId when one column is at fault) describing the most recent rejection.
Nothing latches: every rejection replaces the record, and a slot clears on its
own when a valid value lands.
import { useState } from "react";
import { PretableSurface, type PretableRejectedWrites } from "@pretable/react";
function Positions({ rows }: { rows: readonly Position[] }) {
const [rejected, setRejected] = useState<PretableRejectedWrites | null>(null);
return (
<>
{rejected?.rows && <StaleBanner reason={rejected.rows.message} />}
<PretableSurface
ariaLabel="Positions"
rows={rows}
columns={columns}
getRowId={(row) => row.id}
onRejectedWriteChange={setRejected}
/>
</>
);
}In explicit-model mode the same record is model.rejectedWrites on
usePretable's return, and rejections inside useLocalRowModel surface
through it too. Fatal faults — a disposed model, reentrant mutation, or a
foreign error — still throw; only recoverable prop and model writes become
rejections.
Streaming guidance
Use the rows prop for normal React updates. For high-frequency streams, own a row model and connect @pretable/stream-adapter to it. The adapter animation-frame batches transactions while the surface requests only its indexed viewport range.