Grid Custom rendering with `usePretable`

Custom rendering with `usePretable`

usePretable exposes the engine state so you can render rows your own way.

usePretable returns the engine's full state: the grid model (with interaction methods), the snapshot (sort, filters, selection, focus), the render snapshot (which rows to render at what positions), and telemetry. You write your own JSX on top, applying the [data-pretable-*] attribute contract so @pretable/ui/grid.css styles the result.

This page walks through a minimal-but-complete example.

When to use it

  • You need real sort/filter/selection UI in your app
  • You want custom cell rendering (different React components per column)
  • You're wrapping the engine for your design system
  • You need pinned columns rendered as sticky elements

If none of these apply, the <Pretable> drop-in is simpler.

The hook

ts
import { usePretable } from "@pretable/react"; const { grid, snapshot, renderSnapshot, telemetry } = usePretable({ columns, rows, viewportHeight, // optional: viewportWidth, overscan, state, // { sort?, filters?, selection?, focus? } — controlled state measuredHeights, onSelectionChange, onFocusChange, });

What you get back:

  • grid — the grid model. Methods: setSort(columnId, direction), setFilter(columnId, value), clearFilters(), replaceFilters(map), setSelection(state), selectAll(), clearSelection(), addRange(range), extendRangeFromAnchor(addr), toggleRowSelection(id), setSelectAllVisible(checked), setFocus({rowId, columnId} | null), moveFocus(direction, options?), setViewport({scrollTop, scrollLeft, height, width}), applyTransaction({add, update, remove}).
  • snapshot — current state. Shape: {viewport, sort, filters, selection, focus, totalRowCount, visibleRows, visibleRange}.
  • renderSnapshot — what to render right now. Shape: {columns: PlannedColumn[], rows: PretableRenderRow[], nodeCount, totalHeight, totalWidth}. Each PretableRenderRow has {id, row, rowIndex, top, height}.
  • telemetry{focusedRowId, rowModelRowCount, renderedRowCount, selectedRowId, totalRowCount, ...}.

See API reference for the full type signatures.

A minimal working example

tsx
import { useResolvedHeights, usePretable } from "@pretable/react"; import type { PretableColumn, PretableRow } from "@pretable/react"; interface Person extends PretableRow { id: string; name: string; role: string; city: string; } const columns: PretableColumn<Person>[] = [ { id: "name", header: "Name", value: (r) => r.name, widthPx: 200 }, { id: "role", header: "Role", value: (r) => r.role, widthPx: 200 }, { id: "city", header: "City", value: (r) => r.city, widthPx: 160 }, ]; export function MyGrid({ rows }: { rows: Person[] }) { const { headerHeight } = useResolvedHeights(); const viewportHeight = 480; const { grid, snapshot, renderSnapshot } = usePretable({ columns, rows, viewportHeight, }); const sortedColumn = snapshot.sort.columnId; const sortedDirection = snapshot.sort.direction; return ( <div data-pretable-scroll-viewport="" style={{ height: viewportHeight, overflow: "auto", position: "relative", }} onScroll={(e) => { const el = e.currentTarget; grid.setViewport({ scrollTop: el.scrollTop, scrollLeft: el.scrollLeft, height: viewportHeight, width: el.clientWidth, }); }} > {/* Header row */} <div data-pretable-header-row="" style={{ position: "sticky", top: 0, zIndex: 3, display: "flex", height: headerHeight, minWidth: renderSnapshot.totalWidth, }} > {renderSnapshot.columns.map((col) => { const isSorted = sortedColumn === col.id; const next = isSorted && sortedDirection === "asc" ? "desc" : isSorted && sortedDirection === "desc" ? null : "asc"; return ( <button key={col.id} data-pretable-header-cell="" data-pinned={col.pinned === "left" ? "left" : undefined} onClick={() => grid.setSort(col.id, next)} style={{ position: "absolute", left: col.left, width: col.width, top: 0, height: "100%", border: 0, background: "transparent", textAlign: "left", }} > {columns.find((c) => c.id === col.id)?.header} {isSorted ? (sortedDirection === "asc" ? " ▲" : " ▼") : ""} </button> ); })} </div> {/* Body */} <div data-pretable-scroll-content="" style={{ position: "relative", height: renderSnapshot.totalHeight, minWidth: renderSnapshot.totalWidth, }} > {renderSnapshot.rows.map((row) => { const isSelected = snapshot.selection.ranges.some( (r) => r.startRowId === row.id && r.endRowId === row.id, ); const isFocused = snapshot.focus.rowId === row.id; return ( <div key={row.id} data-pretable-row="" style={{ position: "absolute", top: row.top, height: row.height, left: 0, right: 0, display: "flex", }} onClick={() => { grid.toggleRowSelection(row.id); grid.setFocus({ rowId: row.id, columnId: columns[0]?.id ?? null, }); }} > {renderSnapshot.columns.map((col) => { const column = columns.find((c) => c.id === col.id); const value = column?.value?.(row.row) ?? ""; return ( <div key={col.id} data-pretable-cell="" data-pinned={col.pinned === "left" ? "left" : undefined} data-selected={isSelected ? "true" : "false"} data-focused={ isFocused && snapshot.focus.columnId === col.id ? "true" : "false" } style={{ position: "absolute", left: col.left, width: col.width, height: "100%", boxSizing: "border-box", }} > {String(value)} </div> ); })} </div> ); })} </div> </div> ); }

This renders a working sortable grid with click-to-select. Wire @pretable/ui/themes/excel.css + @pretable/ui/grid.css and the data attributes get styled automatically — gridlines, header bg, selection background, sort indicator color.

What this example does NOT cover

The minimal example above is a starting point. For production grids, you'll likely also need:

  • Keyboard navigation — listen for ArrowUp / ArrowDown on the viewport and call grid.moveFocus("up") / grid.moveFocus("down"). The full keyboard contract (shift+arrow extend, Cmd/Ctrl+arrow jump, Tab wrap, Cmd+A, Esc) is wired by <PretableSurface> automatically.
  • Pinned columns sticky positioning — apply position: sticky; left: ${pinnedOffset}px to pinned cells. The bench's adapter computes pinned offsets via the same algorithm <PretableSurface> uses internally; see packages/react-surface/src/rendering.ts (function getPinnedLeftOffsets) for the canonical implementation.
  • Per-row measured heights — use useLayoutEffect to measure rendered row heights and pass measuredHeights: Record<string, number> to usePretable for content-aware sizing.
  • Filter inputs — render a row of <input> elements above the body, debounce changes, and call grid.setFilter(columnId, value) per change.
  • Telemetry instrumentation — use the telemetry return value to track visible row counts, frame budget overruns, etc.

For the full reference implementation, see packages/react-surface/src/pretable-surface.tsx in the repository — that's what the bench's pretable adapter and the website's playground use internally. It's marked private (lives at @pretable-internal/react-surface), but reading its source is the canonical reference for the patterns above.

Where to go next