This section demonstrates refreshing rows in order to reflect changes at the source while using the Server-Side Row Model (SSRM).
It is advised to use Row IDs when using Server-Side Refresh. Row IDs allow the grid to retain row state between refreshes, such as row height, expanded state, and cell flashing.
Refresh API Copy Link
The grid API refreshServerSide(params) instructs the grid to start reloading all loaded rows for a specified group.
Refresh a server-side store level.
If you pass no parameters, then the top level store is refreshed.
To refresh a child level, pass in the string of keys to get to the desired level.
Once the store refresh is complete, the storeRefreshed event is fired. |
Simple Example Copy Link
To ensure your grid reflects the latest data on your server, you can periodically instruct the grid to refresh all of the loaded rows (known as polling) or strategically refresh based on your applications requirements.
The following example provides a simple demonstration of the different behaviours of the refresh API, note the following:
- Using the Refresh Rows button, you can request that all the rows are requested from the server again, bringing them up to date with the server version.
- Because Row IDs have been implemented, the grid is able to detect which rows have been updated, and flash cells when using
enableCellChangeFlash. - The
Purgecheckbox enables the purge option in the API call, this causes all rows (and all row state except row selection state) to be reset when the refresh call is made, and replaced with loading rows. - When a refresh is finished, note the
storeRefreshedevent is fired, and logged in the console. This is not fired when the purge option is enabled as the rows are reset not refreshed.
("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,
ColDef,
ColGroupDef,
GetRowIdFunc,
GetRowIdParams,
GridApi,
GridOptions,
GridReadyEvent,
HighlightChangesModule,
IServerSideDatasource,
ModuleRegistry,
RowModelType,
StoreRefreshedEvent,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
HighlightChangesModule,
RowGroupingModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
];
let allData: any[];
let versionCounter = 1;
const updateChangeIndicator = () => {
const el = document.querySelector("#version-indicator") as HTMLInputElement;
el.textContent = `${versionCounter}`;
};
const beginPeriodicallyModifyingData = () => {
setInterval(() => {
versionCounter += 1;
allData = allData.map((data) => ({
...data,
version: versionCounter + " - " + versionCounter + " - " + versionCounter,
}));
updateChangeIndicator();
}, 4000);
};
const getServerSideDatasource = (server: any): IServerSideDatasource => {
return {
getRows: (params) => {
console.log("[Datasource] - rows requested by grid: ", params.request);
const response = server.getData(params.request);
const dataWithVersionAndGroupProperties = response.rows.map(
(rowData: any) => {
const rowProperties: any = {
...rowData,
version:
versionCounter + " - " + versionCounter + " - " + versionCounter,
};
// for unique-id purposes in the client, we also want to attach
// the parent group keys
const groupProperties = Object.fromEntries(
params.request.groupKeys.map((groupKey, index) => {
const col = params.request.rowGroupCols[index];
const field = col.id;
return [field, groupKey];
}),
);
return {
...rowProperties,
...groupProperties,
};
},
);
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: dataWithVersionAndGroupProperties,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 1000);
},
};
};
const GridExample = () => {
const gridRef = useRef<AgGridReact>(null);
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "country" },
{ field: "year" },
{ field: "version" },
{ field: "gold", aggFunc: "sum" },
{ field: "silver", aggFunc: "sum" },
{ field: "bronze", aggFunc: "sum" },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 150,
enableCellChangeFlash: true,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
flex: 1,
minWidth: 280,
field: "athlete",
};
}, []);
const getRowId = useCallback((params: GetRowIdParams) => {
const data = params.data;
const parts = [];
if (data.country != null) {
parts.push(data.country);
}
if (data.year != null) {
parts.push(data.year);
}
if (data.id != null) {
parts.push(data.id);
}
return parts.join("-");
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: any[]) => {
// give each data item an ID
const dataWithId = data.map((d: any, idx: number) => ({
...d,
id: idx,
}));
allData = dataWithId;
// setup the fake server with entire dataset
const fakeServer = new FakeServer(allData);
// create datasource with a reference to the fake server
const datasource = getServerSideDatasource(fakeServer);
// register the datasource with the grid
params.api!.setGridOption("serverSideDatasource", datasource);
beginPeriodicallyModifyingData();
});
}, []);
const onStoreRefreshed = useCallback((event: StoreRefreshedEvent) => {
console.log("Refresh finished for store with route:", event.route);
}, []);
const refreshCache = useCallback((route?: string[]) => {
const purge = !!(document.querySelector("#purge") as HTMLInputElement)
.checked;
gridRef.current!.api.refreshServerSide({ route: route, purge: purge });
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div style={{ marginBottom: "5px" }}>
<div>
Version on server: <span id="version-indicator">1</span>
</div>
<button onClick={() => refreshCache(undefined)}>
Refresh Rows
</button>
<label>
<input type="checkbox" id="purge" /> Purge
</label>
</div>
<div style={gridStyle}>
<AgGridReact
ref={gridRef}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
getRowId={getRowId}
rowModelType={"serverSide"}
suppressAggFuncInHeader={true}
onGridReady={onGridReady}
onStoreRefreshed={onStoreRefreshed}
/>
</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%;
}
// This fake server uses http://alasql.org/ to mimic how a real server
// might generate sql queries from the Server-Side Row Model request.
// To keep things simple it does the bare minimum to support the example.
export function FakeServer(allData) {
alasql.options.cache = false;
return {
getData: function (request) {
const results = executeQuery(request);
return {
success: true,
rows: results,
lastRow: getLastRowIndex(request),
};
},
};
function executeQuery(request) {
const sql = buildSql(request);
console.log('[FakeServer] - about to execute query:', sql);
return alasql(sql, [allData]);
}
function buildSql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
function orderBySql(request) {
const sortModel = request.sortModel;
if (sortModel.length === 0) return '';
const sorts = sortModel.map(function (s) {
return s.colId + ' ' + s.sort.toUpperCase();
});
return ' ORDER BY ' + sorts.join(', ');
}
function limitSql(request) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request) {
return executeQuery({ ...request, startRow: undefined, endRow: undefined }).length;
}
}
Refreshing Groups Copy Link
When using row grouping with refreshing you are required to provide a route parameter specifying the row group to refresh. When a row group is refreshed, only its direct child rows are refreshed. This means that in order to refresh the rows in a particular row group, you need to provide the parent of the rows to be refreshed as the route parameter.
The following example demonstrates how to refresh specified groups on the server, note the following:
- Using the Refresh Root Level button, you can force all the rows in the root level group to refresh, this is equivalent to omitting a route parameter from the
refreshServerSideAPI call. - The Refresh ['Canada'] Group button only refreshes the direct children of the
Canadarow group. - The Refresh ['Canada', '2002'] Group button only refreshes the direct children of the
2002row group that belongs to theCanadarow group. - Because Row IDs have been implemented, the grid is able to retain the state for reloaded rows, such as whether a group row was expanded.
- When a refresh is finished, note the
storeRefreshedevent is fired, and logged in the console.
("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,
ColDef,
ColGroupDef,
GetRowIdFunc,
GetRowIdParams,
GridApi,
GridOptions,
GridReadyEvent,
HighlightChangesModule,
IServerSideDatasource,
IsServerSideGroupOpenByDefault,
IsServerSideGroupOpenByDefaultParams,
ModuleRegistry,
RowModelType,
StoreRefreshedEvent,
enableDevValidations,
} from "ag-grid-community";
import {
RowGroupingModule,
ServerSideRowModelApiModule,
ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
HighlightChangesModule,
RowGroupingModule,
ServerSideRowModelModule,
ServerSideRowModelApiModule,
];
let allData: any[];
let versionCounter = 1;
const updateChangeIndicator = () => {
const el = document.querySelector("#version-indicator") as HTMLInputElement;
el.textContent = `${versionCounter}`;
};
const beginPeriodicallyModifyingData = () => {
setInterval(() => {
versionCounter += 1;
allData = allData.map((data) => ({
...data,
version: versionCounter + " - " + versionCounter + " - " + versionCounter,
}));
updateChangeIndicator();
}, 4000);
};
const getServerSideDatasource = (server: any): IServerSideDatasource => {
return {
getRows: (params) => {
console.log("[Datasource] - rows requested by grid: ", params.request);
const response = server.getData(params.request);
const dataWithVersionAndGroupProperties = response.rows.map(
(rowData: any) => {
const rowProperties: any = {
...rowData,
version:
versionCounter + " - " + versionCounter + " - " + versionCounter,
};
// for unique-id purposes in the client, we also want to attach
// the parent group keys
const groupProperties = Object.fromEntries(
params.request.groupKeys.map((groupKey, index) => {
const col = params.request.rowGroupCols[index];
const field = col.id;
return [field, groupKey];
}),
);
return {
...rowProperties,
...groupProperties,
};
},
);
// adding delay to simulate real server call
setTimeout(() => {
if (response.success) {
// call the success callback
params.success({
rowData: dataWithVersionAndGroupProperties,
rowCount: response.lastRow,
});
} else {
// inform the grid request failed
params.fail();
}
}, 1000);
},
};
};
const GridExample = () => {
const gridRef = useRef<AgGridReact>(null);
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "country", hide: true, rowGroup: true },
{ field: "year", hide: true, rowGroup: true },
{ field: "version" },
{ field: "gold", aggFunc: "sum" },
{ field: "silver", aggFunc: "sum" },
{ field: "bronze", aggFunc: "sum" },
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 150,
enableCellChangeFlash: true,
};
}, []);
const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
return {
flex: 1,
minWidth: 280,
field: "athlete",
};
}, []);
const getRowId = useCallback((params: GetRowIdParams) => {
const data = params.data;
const parts = [];
if (data.country != null) {
parts.push(data.country);
}
if (data.year != null) {
parts.push(data.year);
}
if (data.id != null) {
parts.push(data.id);
}
return parts.join("-");
}, []);
const isServerSideGroupOpenByDefault = useCallback(
(params: IsServerSideGroupOpenByDefaultParams) => {
return (
params.rowNode.key === "Canada" ||
params.rowNode.key!.toString() === "2002"
);
},
[],
);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: any[]) => {
// give each data item an ID
const dataWithId = data.map((d: any, idx: number) => ({
...d,
id: idx,
}));
allData = dataWithId;
// setup the fake server with entire dataset
const fakeServer = new FakeServer(allData);
// create datasource with a reference to the fake server
const datasource = getServerSideDatasource(fakeServer);
// register the datasource with the grid
params.api!.setGridOption("serverSideDatasource", datasource);
beginPeriodicallyModifyingData();
});
}, []);
const onStoreRefreshed = useCallback((event: StoreRefreshedEvent) => {
console.log("Refresh finished for store with route:", event.route);
}, []);
const refreshCache = useCallback((route?: string[]) => {
const purge = !!(document.querySelector("#purge") as HTMLInputElement)
.checked;
gridRef.current!.api.refreshServerSide({ route: route, purge: purge });
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div style={{ marginBottom: "5px" }}>
<div>
Version on server: <span id="version-indicator">1</span>
</div>
<button onClick={() => refreshCache(undefined)}>
Refresh Root Level
</button>
<button onClick={() => refreshCache(["Canada"])}>
Refresh ['Canada'] Group
</button>
<button onClick={() => refreshCache(["Canada", "2002"])}>
Refresh ['Canada', '2002'] Group
</button>
<label>
<input type="checkbox" id="purge" /> Purge
</label>
</div>
<div style={gridStyle}>
<AgGridReact
ref={gridRef}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
autoGroupColumnDef={autoGroupColumnDef}
getRowId={getRowId}
isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
rowModelType={"serverSide"}
suppressAggFuncInHeader={true}
onGridReady={onGridReady}
onStoreRefreshed={onStoreRefreshed}
/>
</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%;
}
// This fake server uses http://alasql.org/ to mimic how a real server
// might generate sql queries from the Server-Side Row Model request.
// To keep things simple it does the bare minimum to support the example.
export function FakeServer(allData) {
alasql.options.cache = false;
return {
getData: function (request) {
const results = executeQuery(request);
return {
success: true,
rows: results,
lastRow: getLastRowIndex(request),
};
},
};
function executeQuery(request) {
const sql = buildSql(request);
console.log('[FakeServer] - about to execute query:', sql);
return alasql(sql, [allData]);
}
function buildSql(request) {
return (
selectSql(request) +
' FROM ?' +
whereSql(request) +
groupBySql(request) +
orderBySql(request) +
limitSql(request)
);
}
function selectSql(request) {
const rowGroupCols = request.rowGroupCols;
const valueCols = request.valueCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
const colsToSelect = [rowGroupCol.id];
valueCols.forEach(function (valueCol) {
colsToSelect.push(valueCol.aggFunc + '(' + valueCol.id + ') AS ' + valueCol.id);
});
return 'SELECT ' + colsToSelect.join(', ');
}
return 'SELECT *';
}
function whereSql(request) {
const rowGroups = request.rowGroupCols;
const groupKeys = request.groupKeys;
const whereParts = [];
if (groupKeys) {
groupKeys.forEach(function (key, i) {
const value = typeof key === 'string' ? "'" + key + "'" : key;
whereParts.push(rowGroups[i].id + ' = ' + value);
});
}
if (whereParts.length > 0) {
return ' WHERE ' + whereParts.join(' AND ');
}
return '';
}
function groupBySql(request) {
const rowGroupCols = request.rowGroupCols;
const groupKeys = request.groupKeys;
if (isDoingGrouping(rowGroupCols, groupKeys)) {
const rowGroupCol = rowGroupCols[groupKeys.length];
return ' GROUP BY ' + rowGroupCol.id + ' HAVING count(*) > 0';
}
return '';
}
function orderBySql(request) {
const sortModel = request.sortModel;
if (sortModel.length === 0) return '';
const sorts = sortModel.map(function (s) {
return s.colId + ' ' + s.sort.toUpperCase();
});
return ' ORDER BY ' + sorts.join(', ');
}
function limitSql(request) {
if (request.endRow == undefined || request.startRow == undefined) {
return '';
}
const blockSize = request.endRow - request.startRow;
return ' LIMIT ' + blockSize + ' OFFSET ' + request.startRow;
}
function isDoingGrouping(rowGroupCols, groupKeys) {
// we are not doing grouping if at the lowest level
return rowGroupCols.length > groupKeys.length;
}
function getLastRowIndex(request) {
return executeQuery({ ...request, startRow: undefined, endRow: undefined }).length;
}
}