---
id: "packages/active-grid/server-grouping"
title: "Server grouping"
description: "Grouping on the server: one group path at a time, what the server owns and what the grid owns."
url: "/packages/active-grid/server-grouping"
locale: "en"
revision: "c0de6c8d6a60e4b5"
---

# Server grouping

> [!NOTE]
> **The package's own guide**
>
> This guide is the package's `docs/guides/server-grouping.md`, included
> unchanged from the package as it is in Foundation. Links to the package's
> internal design documents keep their text.

Server grouping fetches one group path at a time. The server owns group
membership, authorization, query execution and aggregate values. The grid owns
expansion controls, loading state, cached children and synthetic display rows.
Client grouping instead runs in the kernel over loaded source records.

## Enabling

Use the resource-backed fetch adapter and server-declared capabilities from
Resource-backed server mode. Grouping must be enabled
both for the grid and for its server mode. This fragment assumes the host
supplies `fetchOrders`, `capabilities`, `columns` and the reader's `locale`:

```tsx
<ActiveGrid
  gridId="orders-by-region"
  locale={locale}
  mode="server"
  fetchFn={fetchOrders}
  serverCapabilities={capabilities}
  columns={columns}
  enableGrouping
  enableServerGrouping
  serverGroupBy={[{field: 'region', sort: 'asc'}, {field: 'category'}]}
/>
```

`serverGroupBy` is an ordered `GroupByColumn[]`, not an array of strings.
The server capability map determines which fields may be grouped and aggregated.
Do not configure the removed `serverGroupAggregations` component prop.

## How requests look

The grouping hook calls `fetchFn` with the ordinary `ServerFetchParams`
(`pageIndex`, `fetchSize`, `sorting`, `filters`, `globalFilter`, `signal`)
plus:

- `groupBy`: ordered `{field, sort?}` declarations;
- `groupKeys`: the path being fetched; `[]` means top level, `['Europe']`
  means children of that region.

There is no `expandedGroups` request field. Expansion state stays in the
outer hook; each fetch asks for one path. The optional historical
`groupAggregations` wire field remains declared but the grid does not populate
it. Resource aggregation policy belongs to the server resource declaration.

`fetchSize` is a fetch-block size, distinct from the visible page size. A
resource group listing returns its complete permitted group listing or refuses
it according to the server's grouping limits; it is not a silently truncated
leaf page. Leaf requests use paging. See Server mode.

## Grouped response

A fetch resolves to `PagedResponse<TData>`, not an action envelope containing
`rows`. The resource adapter unwraps its action response. For grouped results,
`isGrouped: true` and `groups: GroupInfo[]` describe the groups. For example:

```typescript
import type {GroupInfo} from '@workspace/active-grid';

const groups: GroupInfo[] = [
  {
    key: 'Europe',
    count: 42,
    aggregations: {amount: 1250},
    hasSubGroups: true,
  },
];
```

`PagedResponse.data` remains the response data field; `totalCount` is the
count for the requested level. For a leaf result, return its records in
`data` without grouped metadata. The server revalidates the grouping path,
filters and sorting and applies its own authorization at every level.

## Synthetic display rows

The grouping hook constructs a `ServerGroupRow` from each `GroupInfo`:
`__isServerGroup`, `__groupField`, `__groupValue`, `__groupCount`, optional
`__groupAggregations` / `__hasSubGroups`, `__groupKeys` and `__depth`.
These markers describe the outer server-grouping representation. They are
separate from the kernel's client-generated `GeneratedRow` union member.

Each synthetic row is a row of the grid for assistive technology too: it
carries the same logical `aria-rowindex` as the rows around it, so group rows,
loaded records and load-more rows form one unbroken sequence, also in the
mounted window of a virtualized body.

Use `isServerGroupRow(value)` to narrow a server marker and
`isServerLoadMoreRow(value)` for a load-more marker. The outer owner supplies
synthetic IDs before the domain row-ID callback; that callback remains about
real records. Do not pretend a synthetic group is an ordinary domain record.

## Load-more rows

When loaded children do not reach the level's total, the hook creates a
`ServerLoadMoreRow` with `__isServerLoadMore`, `__groupKeys`,
`__loadedCount`, `__totalCount` and `__depth`. Activating it fetches the next
block for that path and appends the accepted children. The server returns
records and counts; it does not need to manufacture this UI marker. A
load-more row stands under a leaf group only.

The row is a row of the grid's keyboard navigation. The arrow keys reach it,
and Enter or Space on any of its cells activates it; a click does the same,
through the same action. It is named by its label ("10 of 42 loaded (load
more)"), its cells carry `aria-keyshortcuts="Enter Space"`, and while the block
is loading the row is `aria-busy` and a second activation asks for nothing.
When the block lands, the active cell keeps its display position, which now
holds the first new row; see Keyboard.

## Rendering aggregations

Group metadata's `aggregations` map becomes `__groupAggregations` on the
synthetic display row. Keys correspond to column IDs. The server-declared
aggregate capability and column presentation semantics determine the display.
A source-record formatter must not be called with a made-up group record;
see the callback contract in Columns. Client grouping
separately requires a declared `aggregationFn` (DC-3).

## State persistence

Server-group expansion is local React state in the grouping hook. It is not
persisted to localStorage or the URL. This differs from the kernel's client
`expanded` slice. The URL owns only `page`, `pageSize`, `sort`, `filters`
and `search`; see URL state.

## Performance

Loaded group children can be reused while their cache entry remains valid.
Sorting, filtering, global search and grouping changes invalidate the grouping
dataset and collapse its local expansion state. Explicit refresh and cache
eviction also prevent a blanket promise of zero refetches. Load-more keeps
leaf fetches bounded; the grid does not load the entire leaf dataset to group it.

## Reference

- Current architecture: kernel and outer ownership.
- Server contract: client declarations and hydration.
- Resource-backed server mode: supported server adapter.
- E2E harness: current grouping fixture navigation.
