Skip to content

BaseGrid runtime and React primitives are exported from @sapporta/grid. Column builders are exported from @sapporta/grid/column-preset. Advanced cursor and controller composition is exported from @sapporta/grid/advanced.

createGridRuntime() returns a GridRuntime. The runtime contains grid-wide state. Each registered path has a GridLevelRuntime for path-local state and commands.

type GridRuntime = {
readonly schema: GridSchema;
readonly interaction: GridInteractionConfig;
readonly root: GridLevelRuntime;
level(path: GridPath): GridLevelRuntime;
registeredLevels(): readonly GridLevelRuntime[];
subscribeLevels(listener: () => void): () => void;
schemaAt(path: GridPath): LevelSchema;
readonly rowOperations: GridRowOperations;
on<E extends keyof GridEvents>(
event: E,
listener: (payload: GridEvents[E]) => void,
): () => void;
dispose(): void;
};

runtime.root is registered during construction. runtime.level(path) returns the current registration for an expanded or retained child path. It throws when the path is not registered. Collapsing a row hides its child levels and retains their resources. runtime.registeredLevels() therefore includes retained collapsed levels.

Schema and interaction values are immutable snapshots. Create a new runtime when either configuration changes.

Every source row provides a stable rowKey. A GridPath identifies one rendered level. A RowId combines the path, displayed row kind, and row key.

type ColId = string;
type RowKey = string;
type GridPath = Brand<string, "GridPath">;
type RowId = Brand<string, "RowId">;
type Coord = { readonly rowId: RowId; readonly colId: ColId };
function rootPath(rootLevelName: string): GridPath;
function childPath(
parent: GridPath,
parentRowKey: RowKey,
childLevelName: string,
): GridPath;
function makeRowId(path: GridPath, rowKey: RowKey): RowId;
function pathOfRowId(id: RowId): GridPath;
function rowKeyOfRowId(id: RowId): RowKey;

Use these helpers instead of constructing or parsing path and row-id strings. Row keys may contain ., #, and %; the identity helpers escape them.

type GridSchema = {
readonly levels: Readonly<Record<string, LevelSchema>>;
readonly rootLevel: string;
};
type LevelSchema = {
readonly name: string;
readonly columns: readonly ColumnSchema[];
readonly rowHeaderColumn:
| { readonly column: ColId }
| "empty-selectable-cell"
| "none";
readonly options: LevelOptions;
readonly childLevels: readonly string[];
};
type TreeNode = {
readonly rowKey: RowKey;
readonly levelName: string;
readonly columns: Readonly<Record<ColId, unknown>>;
readonly rollup?: Readonly<Record<ColId, unknown>>;
readonly children?: Readonly<Record<string, TreeNode | readonly TreeNode[]>>;
readonly childFooterRows?: Readonly<Record<string, readonly FooterRow[]>>;
readonly kind?: "opening" | "closing" | "subtotal";
};

TreeNode.rowKey is required. The schema no longer derives row identity from a column or array index.

const schema = {
rootLevel: "projects",
levels: {
projects: {
name: "projects",
rowHeaderColumn: "none",
columns: projectColumns,
options: {},
childLevels: ["tasks"],
},
tasks: {
name: "tasks",
rowHeaderColumn: "none",
columns: taskColumns,
options: {},
childLevels: [],
},
},
} satisfies GridSchema;
const tree = [
{
rowKey: "project-1",
levelName: "projects",
columns: { name: "Migration" },
},
] satisfies TreeNode[];

Runtime row reads return the LevelRow discriminated union. Branch on row.kind before using kind-specific fields. The union includes data, rollup, opening, closing, subtotal, footer, and phantom rows.

type RuntimeArgs = {
readonly schema: GridSchema;
readonly dataSource: GridDataSource;
readonly interaction?: GridInteractionConfig;
readonly phantoms?: PhantomChannel;
readonly phantomRows?: PhantomRowsConfig;
readonly onLoadedRowsBoundary?: (
event: LoadedRowsBoundaryEvent,
) => Promise<SourceLoadResult> | false;
readonly on?: {
readonly [E in keyof GridEvents]?: (payload: GridEvents[E]) => void;
};
readonly onObserverError?: (error: unknown) => void;
};
function createGridRuntime(args: RuntimeArgs): GridRuntime;

React screens create and dispose the runtime with useGridRuntimeEffect():

const runtime = useGridRuntimeEffect(
() => createGridRuntime({ schema, dataSource }),
[schema, dataSource],
);
if (!runtime) return null;
return (
<GridRuntimeProvider runtime={runtime}>
<GridCopyContextMenu>
<GridLevel path={runtime.root.path} />
</GridCopyContextMenu>
</GridRuntimeProvider>
);

Outside React, call runtime.dispose() from the owner’s cleanup path. Repeated calls are safe.

Resolve a level once, then use its path-bound reads, subscriptions, and commands.

type GridLevelRuntime = {
readonly path: GridPath;
readonly schema: LevelSchema;
readonly data: RuntimeLevelDataSource;
displayedRows(): DisplayedRows;
displayedRowSequence(): DisplayedRowSequence;
displayedRow(rowId: RowId): LevelRow | undefined;
dataRowTarget(rowId: RowId): RowOperationTarget<"data"> | undefined;
subscribeDisplayedRowSequence(listener: () => void): () => void;
subscribeDisplayedRow(rowId: RowId, listener: () => void): () => void;
activeRow(): RowCursor | null;
selectedRows(): RowSelection;
selectedRowIds(): readonly RowId[];
rowInteractionSnapshot(): RowInteractionSnapshot;
subscribeActiveRow(listener: () => void): () => void;
subscribeSelectedRows(listener: () => void): () => void;
subscribeSelectedRowIds(listener: () => void): () => void;
subscribeRowInteractionSnapshot(listener: () => void): () => void;
selectRow(rowId: RowId): void;
setRowSelection(selection: RowSelection): void;
toggleRowSelection(rowId: RowId): void;
extendRowSelectionTo(rowId: RowId): void;
clearRowSelection(): void;
isExpanded(rowId: RowId): boolean;
subscribeExpansion(listener: () => void): () => void;
expand(rowId: RowId): void;
collapse(rowId: RowId): void;
toggleExpand(rowId: RowId): void;
writeCell(coord: Coord, value: unknown): void;
applyChanges(changes: readonly CellChange[]): void;
createRow(node: TreeNode, atIndex?: number): Promise<CreateNodeResult>;
removeRow(rowKey: RowKey): Promise<void>;
readonly drafts: {
get(): readonly PhantomRow[];
subscribe(listener: () => void): () => void;
add(rowKey: RowKey, columns?: Readonly<Record<ColId, unknown>>): void;
remove(rowKey: RowKey): void;
setCell(rowKey: RowKey, colId: ColId, value: unknown): void;
commit(rowKey: RowKey, atIndex?: number): Promise<CreateNodeResult>;
};
};

Dynamic reads, commands, and subscriptions are guarded by the level’s registration lifetime. Static fields remain readable after unregistration.

const level = runtime.root;
const rowId = makeRowId(level.path, "project-1");
level.writeCell({ rowId, colId: "status" }, "done");
level.applyChanges([
{ rowKey: "project-1", colId: "status", value: "done" },
{ rowKey: "project-1", colId: "owner", value: "user-7" },
]);
await level.createRow({
rowKey: "project-2",
levelName: "projects",
columns: { name: "New project" },
});
await level.removeRow("project-2");

runtime.rowOperations builds validated targets from registered levels. Explicit row selection wins per path. Cell-selected rows provide the fallback for targets().

type GridRowOperations = {
targets(): readonly RowOperationTarget[];
selectedDataTargets(): readonly RowOperationTarget<"data">[];
remove(
targets: readonly RowOperationTarget<"data">[],
): Promise<RowRemovalResult>;
};
const result = await runtime.rowOperations.remove(
runtime.rowOperations.selectedDataTargets(),
);
if (result.kind === "partial") {
console.error(result.failed, result.error);
}

Deletion runs child-first and preserves a valid cursor landing. A partial result contains removed, failed, and unattempted targets.

level.data is the read and query facade for the level source. Runtime writes remain on GridLevelRuntime so validation, reconciliation, and host events use one boundary.

const level = runtime.root;
const state = level.data.state();
const unsubscribe = level.data.subscribe(() => console.log(level.data.state()));
await level.data.query?.sort?.set([{ colId: "name", direction: "asc" }]);
await level.data.query?.filter?.set({ archived: false });
await level.data.query?.refetch?.();

The root package omits raw controllers, cursor managers, and renderer internals. Import supported composition helpers from @sapporta/grid/advanced:

import {
cellActivationFor,
controllerFor,
cursorManagerFor,
materializedChildren,
} from "@sapporta/grid/advanced";
const cursors = cursorManagerFor(runtime);
cursors.moveCellCursorTo({
path: runtime.root.path,
rowId: makeRowId(runtime.root.path, "project-1"),
colId: "name",
});
const controller = controllerFor(runtime, runtime.root.path);
controller.startEdit(
{ rowId: makeRowId(runtime.root.path, "project-1"), colId: "name" },
"f2",
);

Advanced helpers are lifetime-checked facades. They do not expose the private runtime kernel.

GridRuntimeProvider supplies the runtime to GridLevel and the public hooks. Common hooks include:

useGridRuntime();
useLevelSnapshot(path);
useDisplayedRowSequence(path);
useDisplayedRow(path, rowId);
useActiveCell();
useActiveCellForPath(path);
useCellSelection(path);
useActiveRow(path);
useSelectedRows(path);
useSelectedRowIds(path);
useRowInteractionSnapshot(path);

Use hooks in React components. Use GridLevelRuntime subscriptions in non-React hosts and custom stores.