Headless engine Snapshot & subscribe
Snapshot & subscribe
The getSnapshot shape, the subscribe contract, and useSyncExternalStore integration.
getSnapshot() returns a PretableGridSnapshot<TRow> — the complete, current state of the engine. subscribe(listener) tells you when it changes. Together they're the whole read side of the engine.
The snapshot shape
| Field | Type | What it is |
|---|---|---|
visibleRows | PretableVisibleRow<TRow>[] | The filtered + sorted row set — rows passing the current filter, in sort order. Not viewport-windowed. |
visibleRange | PretableRowRange | { start, end }. Currently the full range { start: 0, end: visibleRows.length }. |
totalRowCount | number | Count of source rows, before filtering. |
sort | PretableSortState | { columnId, direction } — the active sort. |
filters | Record<string, string> | Active per-column filter values, keyed by column id. |
selection | PretableSelectionState | { anchor, ranges } — the current cell-range selection. |
focus | PretableFocusState | { rowId, columnId } — the focused cell. |
viewport | PretableViewportState | { scrollTop, scrollLeft, width, height }. |
visibleRows
Each entry is a PretableVisibleRow<TRow>:
interface PretableVisibleRow<TRow> {
id: string; // the row id from getRowId
row: TRow; // the source row object
sourceIndex: number; // index of the row in the original rows array
}This is the array you render. It already reflects the active filter and sort, so you map it directly to markup — no client-side filtering or sorting on your end.
visibleRange
visibleRange is { start, end } and is currently always the full range — { start: 0, end: visibleRows.length }. It is reserved for future windowing. Do not treat it as a viewport slice; render the rows in visibleRows, not a slice indexed by visibleRange.
viewport
viewport holds { scrollTop, scrollLeft, width, height }. The engine uses it for one thing only: PageUp / PageDown focus math in moveFocus. It does not decide which rows visibleRows returns — the engine never windows rows by viewport.
The subscribe contract
const unsubscribe = grid.subscribe(() => {
// fires after every mutation
render(grid.getSnapshot());
});
unsubscribe(); // stop listeningsubscribe(listener)returns an unsubscribe function.- The listener fires after each effective state change (a no-op mutation, like re-applying the current sort, doesn't notify).
- Pair it with
getSnapshot()— the listener is the "something changed" signal; the snapshot is the new state.
Snapshot identity is stable
getSnapshot() returns the same object reference until the next mutation — the engine memoizes the snapshot. That stable identity is what makes the store safe for useSyncExternalStore: React can compare snapshots by reference, so a subscribe notification that doesn't change state won't cause a render loop.
Server-side rendering
useSyncExternalStore takes a third argument for the server snapshot. Pass getSnapshot again:
const snapshot = useSyncExternalStore(
grid.subscribe,
grid.getSnapshot,
grid.getSnapshot, // server snapshot
);The engine's snapshot is deterministic from the options you passed to createGrid, so the server render and the first client render produce the same visibleRows — no hydration mismatch.
Next
Actions
Every mutation that changes the snapshot: sort, filter, select, focus, column layout, and data transactions.