# Server-side Row Grouping
URL: https://pro.rv-grid.com/guides/server-side-grouping/
Source: src/content/docs/guides/server-side-grouping.mdx
Description: Group, expand, and lazy-load millions of rows without loading the full dataset into the browser.
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.

## Setup

```ts
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:

```ts
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:

```ts
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

`loadRows(request, signal)` receives:

```ts
{
  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:

```ts
{
  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:

```ts
{
  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

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

```ts
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:

```ts
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

| 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. |
