Streaming updates
Push live values into cells by row id, without a full data replacement.
updateCells writes values straight into the grid by row id, for live feeds that update many cells per second.
Click the Change % header to sort: with Auto-sort off, rows hold their position as new values stream in and a "Re-sort" bar appears once the order goes stale; with Auto-sort on, the grid re-sorts on every tick (4/s).
Usage
Address each write by the row's getRowId value and the column id:
import { useDataGridActions, type CellPatch } from "@/components/data-grid/data-grid";
function TickerFeed() {
const actions = useDataGridActions();
useEffect(() => {
const id = setInterval(() => {
const patches: CellPatch[] = quotes.map((q) => ({
rowId: q.symbol,
columnId: "price",
value: q.price,
}));
actions.updateCells(patches);
}, 250);
return () => clearInterval(id);
}, [actions]);
return null;
}One call is one batch. The grid applies all patches together and fires onDataChange once, with
one update operation per touched row. Rows you do not patch keep their identity and do not render again.
A patch with an unknown rowId or columnId is skipped. A patch to a read-only column, or a
patch whose value equals the current value, is also skipped. A hidden column can be patched. On a
readOnly grid, every batch is a silent no-op.
Row ids, not coordinates
A view coordinate changes when the user sorts or filters. A row id does not, so a feed can write correctly while the user changes the view.
Sorted and filtered views
If a patch changes a value that the active sort or filter reads, a full re-sort moves the row.
By default the row keeps its position, and the grid sets a stale flag. Read the flag with
useDataGridViewStale and give the user a control to re-sort:
function ReSortBar() {
const actions = useDataGridActions();
const viewStale = useDataGridViewStale();
if (!viewStale) return null;
return (
<button onClick={() => actions.reconcileView()}>Re-sort</button>
);
}A direct write raises the same flag: an edit, paste, fill, or cleared selection that touches a column the active sort or filter reads leaves the row where it is and marks the view stale.
reconcileView() rebuilds the order and clears the flag. A sort, filter, or search change does
the same thing.
The reorder option sets this behavior per call:
actions.updateCells(patches, { reorder: "immediate" });| Value | Behavior | When to use |
|---|---|---|
"defer" (default) | The value changes, the row holds its position, and viewStale becomes true. | When a row that jumps position mid-read is worse than a stale order the user clears on demand. |
"immediate" | The grid moves the patched rows to their new positions in the same call. | When the displayed order must always match the sort. |
"never" | The value changes and viewStale stays false. |
If no patched column feeds the active sort or filter, "defer" becomes "never" automatically, so
viewStale stays false for a feed that writes only non-sorted columns.
"immediate" does not re-sort all rows. It removes each patched row from the order, tests it
against the active filter again, and puts it back at the position a full sort gives it. The
cost is set by the number of patched rows, not by the number of rows in the grid.
At 100,000 rows with one sort column, a production build measures:
| Rows in the batch | Cost per call |
|---|---|
| 1 | 0.3-0.5 ms |
| 20 | 0.9 ms |
| 256 | 7.6 ms |
Above 256 rows in one batch, the grid rebuilds the full order instead. It also rebuilds when the sort or filter changed in the same tick, or when a column supplies its own comparator.
The demo's auto-sort switch toggles reorder between the modes on every tick. On, rows on the
sorted column stay in the correct order without a re-sort click.
Controlled mode
updateCells works with data and with defaultData. In controlled mode, store the array that
onDataChange gives you and pass it back unchanged:
const [data, setData] = useState(rows);
<DataGridProvider
data={data}
columns={columns}
getRowId={(row) => row.id}
onDataChange={(next) => setData(next as Row[])}
/>The grid recognizes its own array and keeps the sort, filter, and search results it already has. If you map, filter, or rebuild the array before you store it, the grid cannot recognize it and computes those results again on every update.
Keep the sort and filter props stable
A new sortState or filterState array on each render is a change of input, and the grid
computes the view again. Pass a stable reference.
Undo history
A updateCells batch is tagged source: "stream". useDataGridHistory does not record that
source, because a feed at 100 updates per second removes all user entries from the undo stack.
To make one batch undoable, tag it as an edit:
actions.updateCells(patches, { source: "edit" });To record streams as well, list the sources you want:
useDataGridHistory({
recordSources: ["edit", "paste", "fill", "delete", "row-operation", "import", "stream", "app"],
});See Undo & redo.
What does not change
A updateCells batch without reorder: "immediate" does not move the selection, the active
cell, or an open editor. An immediate re-sort is the exception: it re-sorts the view and
re-anchors the open edit to its row's new view position, keeping the typed draft while the row
stays in the rendered rows. A feed cannot pull the grid away from a user who is editing a
cell.
Values are checked with each column's validate function. A value that fails is skipped, and the
rest of the batch is applied. Set skipValidation: true when the producer already checked the
values.
An async schema holds the batch
A batch into a column with an async validate changes nothing until the schema resolves. The
accepted cells then commit together as one onDataChange. A cell that fails drops silently, and
a batch in which every cell fails commits nothing. A newer updateCells call, or any row-moving
operation, supersedes the held batch; its late verdict is dropped. The full bulk-path contract,
shared by paste, fill, and import, is on
Editing & cell types: async schemas.
What the verdict tells you
updateCells and updateRows return a verdict for every patch in the batch:
const verdict = actions.updateCells(patches);
// { applied: 1, skipped: [{ patchIndex: 1, reason: "no-op" }], pending: false }applied- the number of cells the call wrote.skipped- one entry per patch that was not written, with itspatchIndexinpatchesand areason:unknown-row- the row id is not indata.unknown-column- the column id is not a known column.hole- the row is an unloaded lazy hole.readonly- the column, or the row viareadOnly(row), is read-only.invalid- the column'svalidaterejected the value.no-op- the value equals the current one.
pending-truewhen an async schema holds the batch. Its outcome is not reportable on the return value; the accepted cells commit when the schema resolves.
A batch that writes nothing emits no onDataChange.
Whole rows
updateRows takes a partial row instead of one cell:
actions.updateRows([{ rowId: "AAPL", changes: { price: 214.5, change: 1.2 } }]);Each entry in changes becomes one patch. The options are the same.
See Events & state for the onDataChange payload shape and
Performance for the row and cell render contracts.