Skip to content

Server-side Row Grouping

Server-side row grouping for large datasets lets RevoGrid render grouped data from a backend route API. The browser keeps only visible grouped blocks and cached route blocks, while your server owns grouping, sorting, filtering, quick filtering, counts, and aggregates.

Use it for ERP, financial analytics, logs, operations, and reporting tools where a complete client-side dataset is too large.

  • Group by one or multiple columns.
  • Lazy-load group children on expand.
  • Works with server-side Infinity Scroll semantics.
  • Supports unknown row counts.
  • Keeps browser memory low with block caching.
  • Sends sort, filter, quick-filter, and group state to your backend.
import { ServerSideGroupingPlugin } from '@revolist/revogrid-pro';
const grid = document.createElement('revo-grid');
grid.serverSideGrouping = {
groupBy: ['country', 'sport'],
loadRows: async (request, signal) => {
const response = await fetch('/api/grid/grouping', {
method: 'POST',
signal,
headers: { 'content-type': 'application/json' },
body: JSON.stringify(request),
});
return response.json();
},
};
grid.plugins = [ServerSideGroupingPlugin];

groupBy and loadRows are the only required options. The full configuration can add request tuning, a quick-filter payload, an imperative plugin controller, and an optimized expand-all datasource:

let groupingPluginControllerApi;
grid.serverSideGrouping = {
groupBy: ['country', 'sport'],
blockSize: 100,
purgeClosedGroups: false,
maxConcurrentRequests: 4,
blockLoadDebounceMs: 25,
quickFilter: { text: '' },
api: api => {
groupingPluginControllerApi = api;
},
loadRows,
loadExpandedGroups: async (request, signal) => {
const response = await fetch('/api/grid/grouping/expand-all', {
method: 'POST',
signal,
headers: { 'content-type': 'application/json' },
body: JSON.stringify(request),
});
return response.json();
},
};

api is not a server endpoint. It is a callback that gives the application an imperative controller after the plugin is initialized. Use it from toolbar buttons, route changes, or refresh actions:

groupingPluginControllerApi?.refreshServerSide();
groupingPluginControllerApi?.expandGroup(['Germany']);
groupingPluginControllerApi?.purgeServerSideCache();

loadRows is the required server datasource. It loads one route block at a time: root groups, child groups, or final leaf rows. loadExpandedGroups is optional and only optimizes expandAllGroups() by letting the backend return a flattened group tree in one request.

The plugin owns the main row source while active. InfinityScrollPlugin is complementary in behavior and request shape, but should not also own the same rgRow source. If both plugins are registered and serverSideGrouping is configured, Infinity Scroll no-ops with a warning.

When combining server-side grouping with selection filters, provide filter.selection.getItems from the same backing datasource used by your server loader. The grid source contains generated group rows and only the currently loaded route blocks, so deriving selection options from the visible grid source can produce incomplete values such as only Empty.

loadRows(request, signal) receives:

{
route: ['Germany'],
level: 1,
groupBy: ['country', 'sport'],
start: 0,
limit: 100,
sort: { revenue: 'desc' },
filter: { country: { type: 'contains', value: 'Ger' } },
quickFilter: { text: 'premium' },
parent: { key: 'Germany', count: 12450 },
levelParams: { tenantId: 'acme' }
}

Return group rows before the final grouping level:

{
groups: [
{ key: 'Swimming', count: 2100, aggregates: { Revenue: 'EUR 9.2M' } },
{ key: 'Cycling', count: 1540 }
],
rowCount: 2,
grandTotals: { Revenue: 'EUR 18.7M' }
}

Return leaf rows at the final level:

{
rows: [
{ id: 1, country: 'Germany', sport: 'Swimming', city: 'Berlin', revenue: 1200 }
],
rowCount: 2100,
hasMore: true
}

When rowCount is omitted, RevoGrid treats the count as unknown and keeps a loading tail while hasMore is true.

The API is exposed through the config callback, not by mutating the grid instance.

grid.serverSideGrouping = {
groupBy: ['country'],
loadRows,
api: api => {
groupingPluginControllerApi = api;
groupingPluginControllerApi?.expandGroup(['Germany']);
groupingPluginControllerApi?.expandAllGroups();
groupingPluginControllerApi?.refreshRoute(['Germany']);
groupingPluginControllerApi?.purgeServerSideCache();
console.log(groupingPluginControllerApi?.getServerSideCacheState());
},
};

Available methods:

MethodDescription
expandGroup(id)Expands and lazy-loads a group route.
expandAllGroups()Loads group blocks recursively and expands every server group route. Leaf-row blocks still load from the viewport.
collapseGroup(id)Collapses a group route.
collapseAllGroups()Collapses all group routes currently known by the cache.
refreshServerSide()Invalidates all route blocks and reloads root.
refreshRoute(route)Invalidates only one route branch when possible.
purgeServerSideCache(route?)Clears all cache or one route branch.
getServerSideCacheState()Returns debug route, block, loading, and queue state.

expandAllGroups() opens every group route. It does not load every leaf row, so large final groups stay virtualized and fetch visible row blocks through the normal server-side scroll path.

When loadExpandedGroups is configured, expandAllGroups() uses that single endpoint instead of recursively loading route blocks:

grid.serverSideGrouping = {
groupBy: ['country', 'sport'],
loadRows,
loadExpandedGroups: async request => ({
groups: [
{ route: ['Germany'], key: 'Germany', count: 12450 },
{ route: ['Germany', 'Swimming'], key: 'Swimming', count: 2100 },
{ route: ['Germany', 'Cycling'], key: 'Cycling', count: 1540 },
],
}),
};

Each returned group row must include its full route. The plugin rebuilds group route blocks from this flattened tree and keeps leaf-row blocks unloaded until they enter the viewport.

OptionDefaultDescription
groupByrequiredGrouping column props in server route order.
blockSize100Direct children requested per block.
purgeClosedGroupsfalsePurge child route cache on collapse.
maxConcurrentRequests4Limits simultaneous route block requests.
blockLoadDebounceMs0Delays block load dispatch after scroll/materialization.
quickFilterundefinedGlobal filter payload forwarded to the server.
apiundefinedReceives the client-side plugin controller. This is not a server request callback.
loadRowsrequiredServer datasource for root groups, nested group routes, and final leaf rows.
loadExpandedGroupsundefinedOptional one-request expand-all endpoint that returns flattened group rows with routes.