Skip to main content

Client-side row model

The client-side model keeps row data in the browser and applies filtering, sorting, pagination, grouping, aggregation, and tree relationships locally. It is the default model.

Loading interactive example…
React
<Grid
rowIdKey="id"
rowData={rows}
columnDefs={columns}
pagination
pageSize={25}
pageSizes={[10, 25, 50]}
onGridReady={(api) => {
api.applyTransaction({ add: [newOrder], addIndex: 2 });
}}
/>

Stable row identity

Use rowIdKey when the ID is a field, or getRowId(row) for a derived value. Stable IDs preserve row-node identity across transactions and let editing, selection, grouping, and pinned rows refer to the same record.

const options = {
rowIdKey: "id",
rowModelType: "clientSide", // the default
} satisfies GridOptions;

// Derived or nested IDs:
const derived = {
getRowId: (row) => String(row.account.id),
} satisfies GridOptions;

Replace or transact

api.setRowData(rows) supplies a whole data set; whether that resets the grid or is diffed into it depends on rowDataMode (see below). api.applyTransaction({ add, addIndex, update, remove }) changes only affected rows and returns { added, updated, removed } counts of what was actually applied (unknown row IDs are skipped). Updates preserve node identity; structural changes are sorted and filtered into their correct location.

Use addIndex to insert new rows at a zero-based position in the underlying client-side row order:

api.applyTransaction({
add: [{ id: "c", name: "Gamma" }],
addIndex: 2,
update: [{ rowId: "a", row: { id: "a", name: "Alpha updated" } }],
remove: ["b"],
});

The index is evaluated after removals in the same transaction. It defaults to the end, and out-of-range values are clamped. It is a source-order position: active sorting, filtering, grouping, tree relationships, or pagination may put the inserted row at a different displayed position.

Transactions are client-side only. The server-side model is refreshed through api.refreshServerSideData().

Batch high-frequency updates

For isolated changes, applyTransaction() updates the model and rendered view synchronously. For a stream of small updates, use applyTransactionAsync() so nearby calls share one filter, sort, grouping, aggregation, pagination, and render pass:

const result = await api.applyTransactionAsync({
update: [{ rowId: quote.symbol, row: quote }],
});

Row nodes are mutated immediately and transactions retain call order. The returned promise resolves after the batch is reflected in the derived view and renderer. Until then, direct row-node reads can see new data while sorted, filtered, or grouped positions still describe the previous render.

The first call opens a fixed asyncTransactionWaitMs window (16 ms by default); later calls join it without postponing its deadline. Use api.flushAsyncTransactions() when a caller needs to finalize the view early. Each promise receives the counts for its own transaction. A later synchronous transaction or row-model operation flushes older queued work first; a new rowData replacement subsumes the pending render in its own full refresh.

How a new rowData reference is applied

The React and Angular bindings compare rowData by reference, so the immutable update patterns those frameworks encourage ([...rows], reducers, signals and state setters) hand the grid a replacement array on every change. What the grid does with it depends on rowDataMode:

rowDataModeWhat happens
"diff"The new array is diffed against the current rows by ID and applied as a transaction. Row nodes keep their identity, and undo/redo history and the page index survive.
"reset"The whole data set is re-ingested: undo/redo history is discarded and the grid returns to page 1.
"auto" (default)"diff" when it is available, "reset" otherwise.

Sort, filter, quick-filter, column state, row selection and group expansion survive in either mode.

Diffing needs a stable identity for each row, so it is used only when the client-side row model is in play and rowIdKey or getRowId is set — without one the grid cannot tell an edited row from a new one. Tree data always resets, because its hierarchy is derived from the array rather than from row membership alone. Setting rowDataMode="diff" explicitly when those conditions are not met logs a warning and falls back.

Diffing compares row objects by reference

A row counts as changed only when its object reference differs from the one the grid already holds. That matches immutable updates, where a changed row is a new object. If your application mutates row objects in place and then passes a new array wrapper around them, the diff sees nothing to update and the display goes stale — use rowDataMode="reset" in that case.

api.setRowData(rows) follows the same rules; api.applyTransaction({ add, addIndex, update, remove }) remains the most direct way to express an incremental change when you already know what changed.