High Frequency Updates relates to lots of updates in high succession going into the grid. Every time you update data in the grid, the grid will rework all aggregations, sorts and filters as well as having the browser update its DOM. If you are streaming multiple updates into the grid this can be a bottleneck. High Frequency Updates are achieved in the grid using Async Transactions. Async Transactions allow for efficient high-frequency grid updates.
Async Transactions Copy Link
When you call applyTransactionAsync() the grid will execute the update, along with any other updates you subsequently provide using applyTransactionAsync(), after 50ms. This allows the grid to execute all the transactions in one batch which is more efficient.
Same as applyTransaction except executes asynchronously for efficiency. |
The following example demonstrates updating data using normal transactions and async transactions:
- Normal Update: Calls
applyTransaction()5000 times with each call updating a single row. - Async Update: Calls
applyTransactionAsync()5000 times with each call updating a single row.
"use client";
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
AutoGroupColumnDef,
CellStyleModule,
ClientSideRowModelApiModule,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
GetRowIdFunc,
GetRowIdParams,
GridApi,
GridOptions,
GridReadyEvent,
HighlightChangesModule,
ModuleRegistry,
ValueFormatterParams,
enableDevValidations,
} from "ag-grid-community";
import { RowGroupingModule, RowGroupingPanelModule } from "ag-grid-enterprise";
import { getData, globalRowData } from "./data";
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
const modules = [
ClientSideRowModelApiModule,
CellStyleModule,
ClientSideRowModelModule,
RowGroupingModule,
RowGroupingPanelModule,
HighlightChangesModule,
];
const UPDATE_COUNT = 5000;
function numberCellFormatter(params: ValueFormatterParams) {
return Math.floor(params.value)
.toString()
.replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
}
// picks a row at random and returns an updated copy: the old current value
// becomes the previous value, and a new random current value is generated.
// the updated row is also written back to globalRowData, so the next update
// to that row starts from the latest values.
function createRandomUpdate() {
const index = Math.floor(window.agRandom() * globalRowData.length);
const item = globalRowData[index];
const updatedItem = {
...item,
previous: item.current,
current: Math.floor(window.agRandom() * 100000) + 100,
};
globalRowData[index] = updatedItem;
return updatedItem;
}
function setMessage(msg: string) {
const eMessage = document.querySelector("#eMessage")!;
eMessage.textContent = msg;
}
const GridExample = () => {
const gridRef = useRef<AgGridReact>(null);
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<any[]>();
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
// these are the row groups, so they are all hidden (they are show in the group column)
{
headerName: "Product",
field: "product",
enableRowGroup: true,
rowGroupIndex: 0,
hide: true,
},
{
headerName: "Portfolio",
field: "portfolio",
enableRowGroup: true,
rowGroupIndex: 1,
hide: true,
},
{
headerName: "Book",
field: "book",
enableRowGroup: true,
rowGroupIndex: 2,
hide: true,
},
{ headerName: "Trade", field: "trade", width: 100 },
// all the other columns (visible and not grouped)
{
field: "current",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
field: "previous",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
field: "dealType",
enableRowGroup: true,
},
{
headerName: "Bid",
field: "bidFlag",
enableRowGroup: true,
width: 100,
},
{
headerName: "PL 1",
field: "pl1",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
headerName: "PL 2",
field: "pl2",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
headerName: "Gain-DX",
field: "gainDx",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
headerName: "SX / PX",
field: "sxPx",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
headerName: "99 Out",
field: "_99Out",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
field: "submitterID",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
field: "submitterDealID",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
]);
const getRowId = useCallback(
(params: GetRowIdParams) => String(params.data.trade),
[],
);
const defaultColDef = useMemo<ColDef>(() => {
return {
width: 120,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
width: 250,
};
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
getData();
setRowData(globalRowData);
}, []);
const onNormalUpdate = useCallback(() => {
const startMillis = new Date().getTime();
setMessage("Running Transaction");
for (let i = 0; i < UPDATE_COUNT; i++) {
setTimeout(() => {
// do normal update. update is done before method returns
gridRef.current!.api.applyTransaction({
update: [createRandomUpdate()],
});
}, 0);
}
// print message in next VM turn to allow browser to refresh first.
// we assume the browser executes the timeouts in order they are created,
// so this timeout executes after all the update timeouts created above.
setTimeout(() => {
const duration = new Date().getTime() - startMillis;
setMessage("Transaction took " + duration.toLocaleString() + "ms");
}, 0);
}, []);
const onAsyncUpdate = useCallback(() => {
const startMillis = new Date().getTime();
setMessage("Running Async");
let updatedCount = 0;
for (let i = 0; i < UPDATE_COUNT; i++) {
setTimeout(() => {
// update using async method. passing the callback is
// optional, we are doing it here so we know when the update
// was processed by the grid.
gridRef.current!.api.applyTransactionAsync(
{ update: [createRandomUpdate()] },
resultCallback,
);
}, 0);
}
function resultCallback() {
updatedCount++;
if (updatedCount === UPDATE_COUNT) {
// print message in next VM turn to allow browser to refresh
setTimeout(() => {
const duration = new Date().getTime() - startMillis;
setMessage("Async took " + duration.toLocaleString() + "ms");
}, 0);
}
}
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div style={{ marginBottom: "5px" }}>
<button onClick={onNormalUpdate}>Normal Update</button>
<button onClick={onAsyncUpdate}>Async Update</button>
<span id="eMessage"></span>
</div>
<div style={gridStyle}>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
suppressAggFuncInHeader={true}
rowGroupPanelShow={"always"}
getRowId={getRowId}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
onGridReady={onGridReady}
/>
</div>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
.example-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
#myGrid {
flex: 1 1 0px;
width: 100%;
}
.number {
text-align: right;
}
// a list of the data, that we modify as we go. if you are using an immutable
// data store (such as Redux) then this would be similar to your store of data.
export var globalRowData: any[];
const MIN_BOOK_COUNT = 10;
const MAX_BOOK_COUNT = 20;
const MIN_TRADE_COUNT = 1;
const MAX_TRADE_COUNT = 10;
const products = [
'Palm Oil',
'Rubber',
'Wool',
'Amber',
'Copper',
'Lead',
'Zinc',
'Tin',
'Aluminium',
'Aluminium Alloy',
'Nickel',
'Cobalt',
'Molybdenum',
'Recycled Steel',
'Corn',
'Oats',
'Rough Rice',
'Soybeans',
'Rapeseed',
'Soybean Meal',
'Soybean Oil',
'Wheat',
'Milk',
'Coca',
'Coffee C',
'Cotton No.2',
'Sugar No.11',
'Sugar No.14',
];
const portfolios = ['Aggressive', 'Defensive', 'Income', 'Speculative', 'Hybrid'];
// start the book id's and trade id's at some future random number,
// looks more realistic than starting them at 0
let nextBookId = 62472;
let nextTradeId = 24287;
// build up the test data
export function getData() {
globalRowData = [];
for (let i = 0; i < products.length; i++) {
const product = products[i];
for (let j = 0; j < portfolios.length; j++) {
const portfolio = portfolios[j];
const bookCount = randomBetween(MAX_BOOK_COUNT, MIN_BOOK_COUNT);
for (let k = 0; k < bookCount; k++) {
const book = createBookName();
const tradeCount = randomBetween(MAX_TRADE_COUNT, MIN_TRADE_COUNT);
for (let l = 0; l < tradeCount; l++) {
const trade = createTradeRecord(product, portfolio, book);
globalRowData.push(trade);
}
}
}
}
}
function randomBetween(min: number, max: number) {
return Math.floor(window.agRandom() * (max - min + 1)) + min;
}
function createTradeRecord(product: string, portfolio: string, book: string) {
const current = Math.floor(window.agRandom() * 100000) + 100;
const previous = current + Math.floor(window.agRandom() * 10000) - 2000;
const trade = {
product: product,
portfolio: portfolio,
book: book,
trade: createTradeId(),
submitterID: randomBetween(10, 1000),
submitterDealID: randomBetween(10, 1000),
dealType: window.agRandom() < 0.2 ? 'Physical' : 'Financial',
bidFlag: window.agRandom() < 0.5 ? 'Buy' : 'Sell',
current: current,
previous: previous,
pl1: randomBetween(100, 1000),
pl2: randomBetween(100, 1000),
gainDx: randomBetween(100, 1000),
sxPx: randomBetween(100, 1000),
_99Out: randomBetween(100, 1000),
};
return trade;
}
function createBookName() {
nextBookId++;
return 'GL-' + nextBookId;
}
function createTradeId() {
nextTradeId++;
return nextTradeId;
}
To help understand the interface for applyTransaction() and applyTransactionAsync(), here are both method signatures side by side. The first executes immediately. The second executes sometime later using a callback for providing a result.
// normal applyTransaction takes a RowDataTransaction and returns a RowNodeTransaction
applyTransaction(rowDataTransaction: RowDataTransaction): RowNodeTransaction
// batch takes a RowDataTransaction and the result is provided some time later via a callback
applyTransactionAsync(
rowDataTransaction: RowDataTransaction,
callback?: (res: RowNodeTransaction) => void
): voidUse Async Transactions if you have a high volume of streaming data going into the grid and don't want the grid's rendering and recalculating to be a bottleneck.
Async Transactions Flushed Event Copy Link
Each time the grid executes a batch of Async Transactions, it dispatches an asyncTransactionsFlushed event.
The event contains results attribute, which is a list of all the results for all Transactions that got applied.
This event is useful for debugging or observing how the Async Transactions are applied for learning purposes.
Flush Async Transactions Copy Link
The default wait between executing batches is 50ms. This means when an Async Transaction is provided to the grid, it can take up to 50ms for that transaction to be applied.
Sometimes you may want all transactions to be applied before doing something - for example you may want to select a row in the grid but want to make sure the grid has all the latest row data before doing so.
To make sure the grid has no Async Transactions pending, you can flush the Async Transaction queue. This is done by calling the API flushAsyncTransactions.
It is also possible to change the wait between executing batches from the default 50ms. This is done using the grid property asyncTransactionWaitMillis.
The example below demonstrates setting the wait time and also flushing. Note the following:
- The property
asyncTransactionWaitMillisis set to 4000, thus transactions get flushed every 4 seconds. - The button Flush Transactions will call the API method
flushAsyncTransactions. - Transactions getting added and executed is logged to the console.
- The example listens on event
asyncTransactionsFlushedand logs how many transactions got applied.
("use client");
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
AsyncTransactionsFlushedEvent,
AutoGroupColumnDef,
CellStyleModule,
ClientSideRowModelApiModule,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
GetRowIdFunc,
GetRowIdParams,
GridApi,
GridOptions,
GridReadyEvent,
HighlightChangesModule,
ModuleRegistry,
ValueFormatterParams,
enableDevValidations,
} from "ag-grid-community";
import { RowGroupingModule, RowGroupingPanelModule } from "ag-grid-enterprise";
import { getData, globalRowData } from "./data";
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
const modules = [
ClientSideRowModelApiModule,
CellStyleModule,
ClientSideRowModelModule,
RowGroupingModule,
RowGroupingPanelModule,
HighlightChangesModule,
];
const UPDATE_COUNT = 20;
function numberCellFormatter(params: ValueFormatterParams) {
return Math.floor(params.value)
.toString()
.replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,");
}
function startFeed(api: GridApi) {
let count = 1;
setInterval(() => {
const thisCount = count++;
const updatedIndexes = new Set<number>();
const updatedItems: any[] = [];
for (let i = 0; i < UPDATE_COUNT; i++) {
// pick one row at random, skipping rows already updated in this transaction
const index = Math.floor(window.agRandom() * globalRowData.length);
if (updatedIndexes.has(index)) {
continue;
}
updatedIndexes.add(index);
// the old current value becomes the previous value
const item = globalRowData[index];
const updatedItem = {
...item,
previous: item.current,
current: Math.floor(window.agRandom() * 100000) + 100,
};
// write back, so the next update to this row starts from the latest values
globalRowData[index] = updatedItem;
updatedItems.push(updatedItem);
}
api.applyTransactionAsync({ update: updatedItems }, () => {
console.log("transactionApplied() - " + thisCount);
});
console.log("applyTransactionAsync() - " + thisCount);
}, 500);
}
const GridExample = () => {
const gridRef = useRef<AgGridReact>(null);
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<any[]>();
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
// these are the row groups, so they are all hidden (they are show in the group column)
{
headerName: "Product",
field: "product",
enableRowGroup: true,
rowGroupIndex: 0,
hide: true,
},
{
headerName: "Portfolio",
field: "portfolio",
enableRowGroup: true,
rowGroupIndex: 1,
hide: true,
},
{
headerName: "Book",
field: "book",
enableRowGroup: true,
rowGroupIndex: 2,
hide: true,
},
{ headerName: "Trade", field: "trade", width: 100 },
// all the other columns (visible and not grouped)
{
headerName: "Current",
field: "current",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
headerName: "Previous",
field: "previous",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
headerName: "Deal Type",
field: "dealType",
enableRowGroup: true,
},
{
headerName: "Bid",
field: "bidFlag",
enableRowGroup: true,
width: 100,
},
{
headerName: "PL 1",
field: "pl1",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
headerName: "PL 2",
field: "pl2",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
headerName: "Gain-DX",
field: "gainDx",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
headerName: "SX / PX",
field: "sxPx",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
headerName: "99 Out",
field: "_99Out",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
headerName: "Submitter ID",
field: "submitterID",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
{
headerName: "Submitted Deal ID",
field: "submitterDealID",
width: 200,
aggFunc: "sum",
enableValue: true,
cellClass: "number",
valueFormatter: numberCellFormatter,
cellRenderer: "agAnimateShowChangeCellRenderer",
},
]);
const getRowId = useCallback(
(params: GetRowIdParams) => String(params.data.trade),
[],
);
const defaultColDef = useMemo<ColDef>(() => {
return {
width: 120,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
width: 250,
};
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
getData();
setRowData(globalRowData);
startFeed(params.api);
}, []);
const onAsyncTransactionsFlushed = useCallback(
(e: AsyncTransactionsFlushedEvent) => {
console.log(
"========== onAsyncTransactionsFlushed: applied " +
e.results.length +
" transactions",
);
},
[],
);
const onFlushTransactions = useCallback(() => {
gridRef.current!.api.flushAsyncTransactions();
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div style={{ marginBottom: "5px" }}>
<button onClick={onFlushTransactions}>Flush Transactions</button>
<span id="eMessage"></span>
</div>
<div style={gridStyle}>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
suppressAggFuncInHeader={true}
rowGroupPanelShow={"always"}
asyncTransactionWaitMillis={4000}
getRowId={getRowId}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
onGridReady={onGridReady}
onAsyncTransactionsFlushed={onAsyncTransactionsFlushed}
/>
</div>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
.example-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
#myGrid {
flex: 1 1 0px;
width: 100%;
}
.number {
text-align: right;
}
const MIN_BOOK_COUNT = 10;
const MAX_BOOK_COUNT = 20;
const MIN_TRADE_COUNT = 1;
const MAX_TRADE_COUNT = 10;
const products = [
'Palm Oil',
'Rubber',
'Wool',
'Amber',
'Copper',
'Lead',
'Zinc',
'Tin',
'Aluminium',
'Aluminium Alloy',
'Nickel',
'Cobalt',
'Molybdenum',
'Recycled Steel',
'Corn',
'Oats',
'Rough Rice',
'Soybeans',
'Rapeseed',
'Soybean Meal',
'Soybean Oil',
'Wheat',
'Milk',
'Coca',
'Coffee C',
'Cotton No.2',
'Sugar No.11',
'Sugar No.14',
];
const portfolios = ['Aggressive', 'Defensive', 'Income', 'Speculative', 'Hybrid'];
// start the book id's and trade id's at some future random number,
// looks more realistic than starting them at 0
let nextBookId = 62472;
let nextTradeId = 24287;
// a list of the data, that we modify as we go. if you are using an immutable
// data store (such as Redux) then this would be similar to your store of data.
export var globalRowData: any[];
// build up the test data
export function getData() {
globalRowData = [];
for (let i = 0; i < products.length; i++) {
const product = products[i];
for (let j = 0; j < portfolios.length; j++) {
const portfolio = portfolios[j];
const bookCount = randomBetween(MAX_BOOK_COUNT, MIN_BOOK_COUNT);
for (let k = 0; k < bookCount; k++) {
const book = createBookName();
const tradeCount = randomBetween(MAX_TRADE_COUNT, MIN_TRADE_COUNT);
for (let l = 0; l < tradeCount; l++) {
const trade = createTradeRecord(product, portfolio, book);
globalRowData.push(trade);
}
}
}
}
}
function randomBetween(min: number, max: number) {
return Math.floor(window.agRandom() * (max - min + 1)) + min;
}
function createTradeRecord(product: string, portfolio: string, book: string) {
const current = Math.floor(window.agRandom() * 100000) + 100;
const previous = current + Math.floor(window.agRandom() * 10000) - 2000;
const trade = {
product: product,
portfolio: portfolio,
book: book,
trade: createTradeId(),
submitterID: randomBetween(10, 1000),
submitterDealID: randomBetween(10, 1000),
dealType: window.agRandom() < 0.2 ? 'Physical' : 'Financial',
bidFlag: window.agRandom() < 0.5 ? 'Buy' : 'Sell',
current: current,
previous: previous,
pl1: randomBetween(100, 1000),
pl2: randomBetween(100, 1000),
gainDx: randomBetween(100, 1000),
sxPx: randomBetween(100, 1000),
_99Out: randomBetween(100, 1000),
};
return trade;
}
function createBookName() {
nextBookId++;
return 'GL-' + nextBookId;
}
function createTradeId() {
nextTradeId++;
return nextTradeId;
}