I'm facing an issue where applying filters in my AG Grid with server-side row grouping is triggering multiple API calls. In some cases, it even results in continuous API calls.
This only happens when row grouping is enabled. I have another component with the same server-side setup but without row grouping, and that works perfectly—only one API call is triggered per filter change.
From debugging, I noticed that the useEffect responsible for calling refreshServerSide() is being triggered only once, but the actual issue seems to lie inside the getServerSideDatasource function. Specifically, params.request is returned twice when I apply a single filter, resulting in two API calls.
Here is the relevant part of my component:
const memoizedFilters = useMemo(() => filters, [JSON.stringify(filters)]);
useEffect(() => {
if(filtersRef.current !== filters) {
filtersRef.current = filters;
if (gridApiRef.current && !gridApiRef.current.isDestroyed()) {
console.log("Refreshing server-side data with new filters");
gridApiRef.current.refreshServerSide();
}
}
}, [memoizedFilters]);
const autoGroupColumnDef = useMemo<ColDef>(() => ({
headerName: "Group",
cellRendererParams: {
suppressCount: false,
suppressDoubleClickExpand: false,
innerRenderer: () => "",
},
}), []);
const getServerSideDatasource = (): IServerSideDatasource => {
return {
getRows: async (params: IServerSideGetRowsParams) => {
console.log("Requesting rows", params.request);
const newStartRow = params.request.startRow ?? 0;
const newEndRow = params.request.endRow ?? 10;
if (params.request.groupKeys.length && params.request.groupKeys[0] !== "") {
const campaign_id = params.request.groupKeys[0];
const campaignFilters = filtersRef.current;
try {
const res = await fetchCampaignOverview({ startRow: newStartRow, endRow: newEndRow, campaign_id, ...campaignFilters });
if (res.rows.length) {
params.success({ rowData: res.rows, rowCount: res.totalRows });
} else {
params.success({ rowData: [], rowCount: 0 });
}
} catch (error) {
console.error("Error fetching rows:", error);
params.fail();
}
} else if (params.request.groupKeys[0] === "") {
const unGroupedData = fetchedDataRef.current.filter((item) => item.campaign_id === "");
console.log("Fetching ungrouped data", unGroupedData);
params.success({ rowData: unGroupedData, rowCount: unGroupedData.length });
} else {
try {
const campaignFilters = filtersRef.current;
const res = await fetchCampaignOverview({ startRow: newStartRow, endRow: newEndRow, ...campaignFilters });
if (res.rows.length) {
const unbalancedData = res.rows.map((item) => ({
...item,
campaign_id: item.has_child ? item.campaign_id : "",
}));
fetchedDataRef.current = unbalancedData;
const groupedData = unbalancedData.filter((item) => item.campaign_id !== "");
const unGroupedData = unbalancedData.filter((item) => item.campaign_id === "");
if (unGroupedData.length > 0) groupedData.push(unGroupedData[0]);
params.success({ rowData: groupedData, rowCount: res.totalRows });
} else {
params.success({ rowData: [], rowCount: 0 });
}
} catch (error) {
params.fail();
console.error("Error fetching rows:", error);
}
}
},
};
};
const onGridReady = (params: GridReadyEvent) => {
console.log("Grid is ready");
if (!gridApiRef.current || gridApiRef.current.isDestroyed()) {
gridApiRef.current = params.api
const datasource = getServerSideDatasource();
params.api.setGridOption("serverSideDatasource", datasource);
}
};
const paginationPageSizeSelector = useMemo(() => [10, 20, 50, 100], []);
return (
<>
<CampaignFilters
onFilterChange={(filters: CampaignFilter) => { setFilters(filters); }}
/>
<TableContainer>
<AgGridReact
autoGroupColumnDef={autoGroupColumnDef}
cacheBlockSize={paginationPageSize}
columnDefs={colDefs}
defaultColDef={defaultColDef}
domLayout="autoHeight"
blockLoadDebounceMillis={1000}
// getServerSideGroupKey={(params: { data: CampaignOverviewRowData }) => params.data.campaign_id || ""}
groupAllowUnbalanced={true}
onGridReady={onGridReady}
onPaginationChanged={() => {
if (gridApiRef.current) {
const pageSize = gridApiRef.current.paginationGetPageSize();
setPaginationPageSize(pageSize);
}
}}
paginateChildRows={true}
pagination={true}
paginationPageSize={10}
paginationPageSizeSelector={paginationPageSizeSelector}
rowModelType="serverSide"
/>
</TableContainer>
</>
);
};
What I’ve Tried:
Checked that refreshServerSide() is only triggered once.
Verified that params.request is returned twice on filter change (causing two API calls).
Compared it with a component that uses the same filtering logic without row grouping, which works fine.
Expected Behavior: Only one API call should be made when applying a filter.
Actual Behavior: Two or more API calls are made when applying a single filter, and in some cases, it continues to call the API indefinitely.