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.
Request Contract
Section titled “Request Contract”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.
Runtime API
Section titled “Runtime API”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:
| Method | Description |
|---|---|
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.
Configuration
Section titled “Configuration”| Option | Default | Description |
|---|---|---|
groupBy | required | Grouping column props in server route order. |
blockSize | 100 | Direct children requested per block. |
purgeClosedGroups | false | Purge child route cache on collapse. |
maxConcurrentRequests | 4 | Limits simultaneous route block requests. |
blockLoadDebounceMs | 0 | Delays block load dispatch after scroll/materialization. |
quickFilter | undefined | Global filter payload forwarded to the server. |
api | undefined | Receives the client-side plugin controller. This is not a server request callback. |
loadRows | required | Server datasource for root groups, nested group routes, and final leaf rows. |
loadExpandedGroups | undefined | Optional one-request expand-all endpoint that returns flattened group rows with routes. |