gridcn1.3.0

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).

Symbol
Price
Change %
Volume
AAPL
100
0
1000000
MSFT
117.5
0
1025000
NVDA
135
0
1050000
AMZN
152.5
0
1075000
GOOG
170
0
1100000
META
187.5
0
1125000
TSLA
205
0
1150000
AMD
222.5
0
1175000
INTC
240
0
1200000
NFLX
257.5
0
1225000

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" });
ValueBehaviorWhen 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 batchCost per call
10.3-0.5 ms
200.9 ms
2567.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 its patchIndex in patches and a reason:
    • unknown-row - the row id is not in data.
    • unknown-column - the column id is not a known column.
    • hole - the row is an unloaded lazy hole.
    • readonly - the column, or the row via readOnly(row), is read-only.
    • invalid - the column's validate rejected the value.
    • no-op - the value equals the current one.
  • pending - true when 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.

On this page