Headless engine Actions

Actions

Apply typed row-model commands and separate UI-state commands.

Data and derivation commands belong to rowModel. Focus, selection, editing, viewport, and visual layout belong to grid.

Rows and atomic transactions

Clicking the button below runs one applyTransaction call that adds a task, marks another done, and tries to remove an id that was never there — all as a single revision:

Atomic transactions

One applyTransaction call adds a row, updates a row, and attempts to remove one that never existed — all landing as a single revision, with the unknown removal reported as a non-fatal issue.

.md
ts
rowModel.setRows(nextRows); // authoritative source order
 
rowModel.applyTransaction({
  add: [{ id: "svc-99", name: "service-99", team: "core" }],
  update: [{ id: "svc-1", changes: { status: "down" } }],
  remove: ["svc-2"],
});

Updates use the exact { id, changes } shape. One call publishes one atomic revision and returns counts, issues, and previous/current revisions — the readout above prints exactly those fields after each click. Unknown update/removal IDs are reported as non-fatal issues rather than thrown, which is what turns the example's always-missing task-ghost removal into an unknown-remove-id issue instead of an error.

In React's declarative mode, update the rows prop instead. Use an explicit row model for imperative or high-frequency producers.

Query

Filters, ordered sorting, and grouping are one typed query value:

ts
const transition = rowModel.setQuery({
  filters: [{ columnId: "team", operator: "contains", value: "payments" }],
  sort: [{ columnId: "latencyMs", direction: "asc" }],
  rowGroups: [],
});
 
await transition.finished;

Query rebuilds publish atomically. A newer query supersedes an older candidate; cancel() rejects that transition without exposing partial results.

Expansion

ts
rowModel.setGroupExpanded(groupId, true);
rowModel.setExpansionDefault({ kind: "through-depth", depth: 1 });
rowModel.expandAll();
rowModel.collapseAll();

Distinct values

Typing below cancels whichever distinctValues request is still in flight and starts a new one, so a slow early keystroke can never overwrite a faster later one:

Distinct-value search

A team search box calls rowModel.distinctValues on every keystroke, cancelling the previous request so a slow early result can never overwrite a faster later one.

.md
ts
const request = rowModel.distinctValues("team", { search: "pay", limit: 50 });
const result = await request.finished;
 
request.cancel(); // rejects `finished` with a cancellation error

Distinct-value lookup is asynchronous and cancellable — the example's activeRequest.current?.cancel() on every keystroke is that cancellation in practice, and its .catch() is there because a cancelled request rejects finished, not because failure is expected.

UI commands

ts
grid.setViewport({ scrollTop, scrollLeft, width, height });
grid.setFocus({ ref: { kind: "data", rowId }, columnId });
grid.moveFocus("down");
grid.toggleRowSelection(rowId);
grid.selectRowRange(firstId, lastId);
grid.selectAllVisibleRows();
grid.clearSelection();
grid.setColumnWidth("name", 240);
grid.setColumnPinned("name", "left");
grid.setColumnOrder(["name", "team", "status"]);

Selection is indexed and row-ID based; it does not require enumerating the complete visible model.

Lifecycle

Dispose both long-lived stores when their owner is destroyed:

ts
grid.dispose();
rowModel.dispose();

Next

API reference

The core factory and contract summary.