---
title: "SSRM Row Grouping"
enterprise: true
framework: react
version: "36.1.0"
---

# SSRM Row Grouping

This section covers Row Grouping in the Server-Side Row Model (SSRM).

## Enabling Row Grouping

Row Grouping is enabled in the grid via the `rowGroup` column definition attribute. The example below shows how to group rows by 'country':

```jsx
const [columnDefs, setColumnDefs] = useState([
    { field: 'country', rowGroup: true },
    { field: 'sport' },
    { field: 'year' },
]);

<AgGridReact columnDefs={columnDefs} />
```

For more configuration details see the section on [Row Grouping](https://www.ag-grid.com/react-data-grid/grouping/).

## Server Side Row Grouping

The actual grouping of rows is performed on the server when using the SSRM. When the grid needs more rows it makes a request via `getRows(params)` on the [Server-Side Datasource](https://www.ag-grid.com/react-data-grid/server-side-model-datasource/) with metadata containing grouping details.

The properties relevant to Row Grouping in the request are shown below:

```ts
type IServerSideGetRowsRequest = {
    // row group columns
    rowGroupCols: ColumnVO[];

    // what groups the user is viewing
    groupKeys: string[];

    // ... // other params
}
```

Note in the snippet above the property `rowGroupCols` contains all the columns (dimensions) the grid is grouping on, e.g. 'Country', 'Year'. The property `groupKeys` contains the list of group keys selected, e.g. `['Argentina', '2012']`.

The example below demonstrates server-side Row Grouping. Note the following:

- **Country** and **Sport** columns have `rowGroup=true` defined on their column definitions. This tells the grid there are two levels of grouping, one for Country and one for Sport.
- The `rowGroupCols` and `groupKeys` properties in the request are used by the server to perform grouping.
- Open the browser's dev console to view the request supplied to the datasource.

#### Row Grouping

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  ModuleRegistry,
  RowModelType,
  enableDevValidations,
} from "ag-grid-community";
import {
  RowGroupingModule,
  ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [RowGroupingModule, ServerSideRowModelModule];

const getServerSideDatasource: (server: any) => IServerSideDatasource = (
  server: any,
) => {
  return {
    getRows: (params) => {
      console.log("[Datasource] - rows requested by grid: ", params.request);
      const response = server.getData(params.request);
      // adding delay to simulate real server call
      setTimeout(() => {
        if (response.success) {
          // call the success callback
          params.success({
            rowData: response.rows,
            rowCount: response.lastRow,
          });
        } else {
          // inform the grid request failed
          params.fail();
        }
      }, 1000);
    },
  };
};

const GridExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "country", rowGroup: true, hide: true },
    { field: "sport", rowGroup: true, hide: true },
    { field: "year", minWidth: 100 },
    { field: "gold", aggFunc: "sum", enableValue: true },
    { field: "silver", aggFunc: "sum", enableValue: true },
    { field: "bronze", aggFunc: "sum", enableValue: true },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      flex: 1,
      minWidth: 120,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      flex: 1,
      minWidth: 280,
      field: "athlete",
    };
  }, []);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: IOlympicData[]) => {
        // setup the fake server with entire dataset
        const fakeServer = new FakeServer(data);
        // create datasource with a reference to the fake server
        const datasource = getServerSideDatasource(fakeServer);
        // register the datasource with the grid
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div style={gridStyle}>
          <AgGridReact<IOlympicData>
            columnDefs={columnDefs}
            defaultColDef={defaultColDef}
            autoGroupColumnDef={autoGroupColumnDef}
            rowModelType={"serverSide"}
            cacheBlockSize={5}
            onGridReady={onGridReady}
          />
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Row Grouping](https://www.ag-grid.com/examples/server-side-model-grouping/row-grouping/reactFunctionalTs)

## Open by Default

It is possible to have rows open as soon as they are loaded. To do this implement the grid callback `isServerSideGroupOpenByDefault`.

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `isServerSideGroupOpenByDefault` | `IsServerSideGroupOpenByDefault` |  |  | Allows groups to be open by default. Module: [`ServerSideRowModelModule`](https://www.ag-grid.com/react-data-grid/modules/). |

```js
// Example implementation
function isServerSideGroupOpenByDefault(params) {
    var rowNode = params.rowNode;
    var isZimbabwe = rowNode.field == 'country' && rowNode.key == 'Zimbabwe';
    return isZimbabwe;
}
```

> **Note**
>
> Server-Side Open By Default requires [Row IDs](https://www.ag-grid.com/react-data-grid/server-side-model-configuration/#providing-row-ids) to be supplied to the grid.

It may also be helpful to use the [Row Node](https://www.ag-grid.com/react-data-grid/row-object/) API `getRoute()` to inspect the route of a row node.

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `getRoute` | `Function` |  |  | Returns the route of the row node. If the Row Node does not have a key (i.e it's a leaf row inside a row group) returns undefined |

Below shows `isServerSideGroupOpenByDefault()` and `getRoute` in action. Note the following:

- The callback opens the following routes as soon as those routes are loaded:
  - **[Zimbabwe]**
  - **[Zimbabwe, Swimming]**
  - **[United States, Swimming]**
- Note **[Zimbabwe]** and **[Zimbabwe, Swimming]** are visibly open by default.
- Note **[United States, Swimming]** is not visibly open by default, as the parent group 'United States' is not open. However when 'United States' is opened, it's 'Swimming' group is opened by default.
- Selecting a group row and clicking 'Route of Selected' prints the route to the selected node to the developer console.

#### Open by Default

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GetRowIdFunc,
  GetRowIdParams,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IsServerSideGroupOpenByDefault,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  RowModelType,
  RowSelectionOptions,
  enableDevValidations,
} from "ag-grid-community";
import {
  RowGroupingModule,
  ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [RowGroupingModule, ServerSideRowModelModule];

const getServerSideDatasource: (server: any) => IServerSideDatasource = (
  server: any,
) => {
  return {
    getRows: (params: IServerSideGetRowsParams) => {
      console.log("[Datasource] - rows requested by grid: ", params.request);
      const response = server.getData(params.request);
      // adding delay to simulate real server call
      setTimeout(() => {
        if (response.success) {
          // call the success callback
          params.success({
            rowData: response.rows,
            rowCount: response.lastRow,
          });
        } else {
          // inform the grid request failed
          params.fail();
        }
      }, 400);
    },
  };
};

const GridExample = () => {
  const gridRef = useRef<AgGridReact<IOlympicData>>(null);
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "country", enableRowGroup: true, rowGroup: true, hide: true },
    { field: "sport", enableRowGroup: true, rowGroup: true, hide: true },
    { field: "year", minWidth: 100 },
    { field: "gold", aggFunc: "sum", enableValue: true },
    { field: "silver", aggFunc: "sum", enableValue: true },
    { field: "bronze", aggFunc: "sum", enableValue: true },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      flex: 1,
      minWidth: 120,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      flex: 1,
      minWidth: 280,
    };
  }, []);
  const rowSelection = useMemo<
    RowSelectionOptions | "single" | "multiple"
  >(() => {
    return { mode: "multiRow" };
  }, []);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: IOlympicData[]) => {
        // setup the fake server with entire dataset
        const fakeServer = new FakeServer(data);
        // create datasource with a reference to the fake server
        const datasource = getServerSideDatasource(fakeServer);
        // register the datasource with the grid
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  const onBtRouteOfSelected = useCallback(() => {
    const selectedNodes = gridRef.current!.api.getSelectedNodes();
    selectedNodes.forEach(function (rowNode, index) {
      const route = rowNode.getRoute();
      const routeString = route ? route.join(",") : undefined;
      console.log("#" + index + ", route = [" + routeString + "]");
    });
  }, []);

  const getRowId = useCallback((params: GetRowIdParams) => {
    return window.agRandom().toString();
  }, []);

  const isServerSideGroupOpenByDefault = useCallback(
    (params: IsServerSideGroupOpenByDefaultParams) => {
      const route = params.rowNode.getRoute();
      if (!route) {
        return false;
      }
      const routeAsString = route.join(",");
      const routesToOpenByDefault = [
        "Zimbabwe",
        "Zimbabwe,Swimming",
        "United States,Swimming",
      ];
      return routesToOpenByDefault.indexOf(routeAsString) >= 0;
    },
    [],
  );

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div className="example-wrapper">
          <div style={{ marginBottom: "5px" }}>
            <button onClick={onBtRouteOfSelected}>Route of Selected</button>
          </div>

          <div style={gridStyle}>
            <AgGridReact<IOlympicData>
              ref={gridRef}
              columnDefs={columnDefs}
              defaultColDef={defaultColDef}
              autoGroupColumnDef={autoGroupColumnDef}
              rowModelType={"serverSide"}
              rowSelection={rowSelection}
              getRowId={getRowId}
              isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
              onGridReady={onGridReady}
            />
          </div>
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Open by Default](https://www.ag-grid.com/examples/server-side-model-grouping/open-by-default/reactFunctionalTs)

## Group Total Rows

To enable [Group Total Rows](https://www.ag-grid.com/react-data-grid/aggregation-total-rows/), set the `groupTotalRow` property to 'top' or 'bottom'.

#### Group Totals

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GetRowIdFunc,
  GetRowIdParams,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IsServerSideGroupOpenByDefault,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  RowModelType,
  UseGroupTotalRow,
  enableDevValidations,
} from "ag-grid-community";
import {
  RowGroupingModule,
  ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [RowGroupingModule, ServerSideRowModelModule];

const getServerSideDatasource: (server: any) => IServerSideDatasource = (
  server: any,
) => {
  return {
    getRows: (params: IServerSideGetRowsParams) => {
      console.log("[Datasource] - rows requested by grid: ", params.request);
      const response = server.getData(params.request);
      // adding delay to simulate real server call
      setTimeout(() => {
        if (response.success) {
          // call the success callback
          params.success({
            rowData: response.rows,
            rowCount: response.lastRow,
          });
        } else {
          // inform the grid request failed
          params.fail();
        }
      }, 400);
    },
  };
};

const GridExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "country", enableRowGroup: true, rowGroup: true, hide: true },
    { field: "sport", enableRowGroup: true, rowGroup: true, hide: true },
    { field: "year", minWidth: 100 },
    { field: "gold", aggFunc: "sum", enableValue: true },
    { field: "silver", aggFunc: "sum", enableValue: true },
    { field: "bronze", aggFunc: "sum", enableValue: true },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      flex: 1,
      minWidth: 120,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      flex: 1,
      minWidth: 280,
    };
  }, []);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: IOlympicData[]) => {
        // setup the fake server with entire dataset
        const fakeServer = new FakeServer(data);
        // create datasource with a reference to the fake server
        const datasource = getServerSideDatasource(fakeServer);
        // register the datasource with the grid
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  const getRowId = useCallback((params: GetRowIdParams) => {
    return window.agRandom().toString();
  }, []);

  const isServerSideGroupOpenByDefault = useCallback(
    (params: IsServerSideGroupOpenByDefaultParams) => {
      const route = params.rowNode.getRoute();
      if (!route) {
        return false;
      }
      const routeAsString = route.join(",");
      const routesToOpenByDefault = ["Zimbabwe", "Zimbabwe,Swimming"];
      return routesToOpenByDefault.indexOf(routeAsString) >= 0;
    },
    [],
  );

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div className="example-wrapper">
          <div style={gridStyle}>
            <AgGridReact<IOlympicData>
              columnDefs={columnDefs}
              defaultColDef={defaultColDef}
              autoGroupColumnDef={autoGroupColumnDef}
              rowModelType={"serverSide"}
              groupTotalRow={"bottom"}
              getRowId={getRowId}
              isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
              onGridReady={onGridReady}
            />
          </div>
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Group Totals](https://www.ag-grid.com/examples/server-side-model-grouping/group-footer/reactFunctionalTs)

Group total rows can also be used with `groupDisplayType='multipleColumns'`, as demonstrated in the example below.

#### Multiple Group Columns and Footers

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GetRowIdFunc,
  GetRowIdParams,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IsServerSideGroupOpenByDefault,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  RowGroupingDisplayType,
  RowModelType,
  UseGroupTotalRow,
  enableDevValidations,
} from "ag-grid-community";
import {
  RowGroupingModule,
  ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [RowGroupingModule, ServerSideRowModelModule];

const getServerSideDatasource: (server: any) => IServerSideDatasource = (
  server: any,
) => {
  return {
    getRows: (params: IServerSideGetRowsParams) => {
      console.log("[Datasource] - rows requested by grid: ", params.request);
      const response = server.getData(params.request);
      // adding delay to simulate real server call
      setTimeout(() => {
        if (response.success) {
          // call the success callback
          params.success({
            rowData: response.rows,
            rowCount: response.lastRow,
          });
        } else {
          // inform the grid request failed
          params.fail();
        }
      }, 400);
    },
  };
};

const GridExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "country", enableRowGroup: true, rowGroup: true, hide: true },
    { field: "sport", enableRowGroup: true, rowGroup: true, hide: true },
    { field: "year", minWidth: 100 },
    { field: "gold", aggFunc: "sum", enableValue: true },
    { field: "silver", aggFunc: "sum", enableValue: true },
    { field: "bronze", aggFunc: "sum", enableValue: true },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      flex: 1,
      minWidth: 120,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      flex: 1,
      minWidth: 280,
    };
  }, []);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: IOlympicData[]) => {
        // setup the fake server with entire dataset
        const fakeServer = new FakeServer(data);
        // create datasource with a reference to the fake server
        const datasource = getServerSideDatasource(fakeServer);
        // register the datasource with the grid
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  const getRowId = useCallback((params: GetRowIdParams) => {
    return window.agRandom().toString();
  }, []);

  const isServerSideGroupOpenByDefault = useCallback(
    (params: IsServerSideGroupOpenByDefaultParams) => {
      const route = params.rowNode.getRoute();
      if (!route) {
        return false;
      }
      const routeAsString = route.join(",");
      const routesToOpenByDefault = ["Zimbabwe", "Zimbabwe,Swimming"];
      return routesToOpenByDefault.indexOf(routeAsString) >= 0;
    },
    [],
  );

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div className="example-wrapper">
          <div style={gridStyle}>
            <AgGridReact<IOlympicData>
              columnDefs={columnDefs}
              defaultColDef={defaultColDef}
              autoGroupColumnDef={autoGroupColumnDef}
              rowModelType={"serverSide"}
              groupTotalRow={"bottom"}
              groupDisplayType={"multipleColumns"}
              getRowId={getRowId}
              isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
              onGridReady={onGridReady}
            />
          </div>
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Multiple Group Columns and Footers](https://www.ag-grid.com/examples/server-side-model-grouping/group-footer-multiple-cols/reactFunctionalTs)

## Grand Total Row

To display a grand total row, set the `grandTotalRow` property to `'top'`, `'bottom'`, `'pinnedTop'`, or `'pinnedBottom'`. The grand total row is supported for both flat grids and grids with row grouping.

### Providing Grand Total Data

When `grandTotalRow` is set, the `needsGrandTotal` hint on `getRows` params will be `true` for root-level requests that don't yet have cached grand total data — this happens on first load and after any filter or aggregation change that purges the cached data. The server may also always provide updated grand total data regardless of this hint. Sort changes do not invalidate the grand total (sorting does not affect the totals), so `needsGrandTotal` remains `false` after a sort-only change.

The `grandTotalData` field on the `success` callback params controls the grand total row:

- Pass the **grand total data object** to set or update the grand total row.
- Pass `null` to explicitly remove an existing grand total row.
- Leave it `undefined` (or omit the field entirely) to keep the grand total unchanged — the grid will continue to show whatever grand total is already cached. This lets paged block requests return data rows without having to re-send the grand total every time.

The example below shows a grouped grid with aggregations on the medal columns. Note how the grand total is recomputed server-side when filters change (reflecting the filtered totals), and that changing an aggregation function via the column menu triggers a fresh request for both data and grand total:

#### Grand Total via getRows

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  ModuleRegistry,
  NumberFilterModule,
  RowModelType,
  SideBarDef,
  TextFilterModule,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  RowGroupingModule,
  RowGroupingPanelModule,
  ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [
  ColumnMenuModule,
  ColumnsToolPanelModule,
  NumberFilterModule,
  RowGroupingModule,
  RowGroupingPanelModule,
  ServerSideRowModelModule,
  TextFilterModule,
];

const getServerSideDatasource: (
  server: ReturnType<typeof FakeServer>,
) => IServerSideDatasource = (server: ReturnType<typeof FakeServer>) => {
  return {
    getRows: (params) => {
      console.log("[Datasource] - rows requested by grid: ", params.request);
      const response = server.getData(params.request, params.needsGrandTotal);
      // Delay long enough for the loading rows to be clearly visible, simulating a remote call.
      setTimeout(() => {
        if (response.success) {
          params.success({
            rowData: response.rows,
            rowCount: response.lastRow,
            grandTotalData: response.grandTotalData,
          });
        } else {
          params.fail();
        }
      }, 800);
    },
  };
};

const GridExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "country", rowGroup: true, hide: true },
    { field: "sport", rowGroup: true, hide: true },
    {
      field: "year",
      minWidth: 100,
      filter: "agNumberColumnFilter",
      floatingFilter: true,
    },
    {
      field: "gold",
      aggFunc: "sum",
      enableValue: true,
      filter: "agNumberColumnFilter",
      floatingFilter: true,
    },
    {
      field: "silver",
      aggFunc: "sum",
      enableValue: true,
      filter: "agNumberColumnFilter",
      floatingFilter: true,
    },
    {
      field: "bronze",
      aggFunc: "sum",
      enableValue: true,
      filter: "agNumberColumnFilter",
      floatingFilter: true,
    },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      flex: 1,
      minWidth: 120,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      flex: 1,
      minWidth: 240,
      field: "athlete",
    };
  }, []);
  const sideBar = useMemo<
    SideBarDef | string | string[] | boolean | null
  >(() => {
    return {
      toolPanels: ["columns"],
    };
  }, []);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: IOlympicData[]) => {
        const fakeServer = new FakeServer(data);
        const datasource = getServerSideDatasource(fakeServer);
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div className="example-wrapper">
          <div style={gridStyle}>
            <AgGridReact<IOlympicData>
              columnDefs={columnDefs}
              defaultColDef={defaultColDef}
              autoGroupColumnDef={autoGroupColumnDef}
              rowModelType={"serverSide"}
              grandTotalRow={"bottom"}
              cacheBlockSize={20}
              sideBar={sideBar}
              onGridReady={onGridReady}
            />
          </div>
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Grand Total via getRows](https://www.ag-grid.com/examples/server-side-model-grouping/grand-total-getrows/reactFunctionalTs)

Alternatively, if `getRowId` is configured, a row with ID `GRAND_TOTAL_ROW_ID` (`'rowGroupFooter_ROOT_NODE_ID'`) included in `rowData` will also be treated as the grand total (the `grandTotalData` field takes priority).

### Updating the Grand Total Row via Transactions

The grand total row can be updated, added, or removed via `applyServerSideTransaction`:

- **Update**: Include the grand total data in the `update` array. The `getRowId` must return `GRAND_TOTAL_ROW_ID` (`'rowGroupFooter_ROOT_NODE_ID'`) for this row.
- **Add**: Include the grand total data in the `add` array. If a grand total already exists, it will be updated.
- **Remove**: Include a row whose `getRowId` returns `GRAND_TOTAL_ROW_ID` (`'rowGroupFooter_ROOT_NODE_ID'`) in the `remove` array to remove it.

This is useful when the grand total comes from a different endpoint than the paged data — for example, when computing totals is more expensive than loading rows and should be done in a separate, independently cancellable request.

The example below demonstrates that pattern on a flat grid. Each `getRows` call fetches only the data rows; whenever the grid signals `needsGrandTotal`, a separate asynchronous request for the grand total is started in parallel. While it is in flight the current grand total is removed via transaction so stale values aren't shown, and when the response arrives the new total is applied via an `add` transaction. A monotonic request id ensures that if a newer grand-total fetch is started before an earlier one returns, the stale response is discarded rather than overwriting fresher data.

#### Grand Total with Transactions

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
  ColDef,
  ColGroupDef,
  GRAND_TOTAL_ROW_ID,
  GetRowIdFunc,
  GetRowIdParams,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  ModuleRegistry,
  NumberFilterModule,
  RowModelType,
  TextFilterModule,
  enableDevValidations,
} from "ag-grid-community";
import {
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { OlympicRow } from "./interfaces";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [
  NumberFilterModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
  TextFilterModule,
];

let fakeServer: ReturnType<typeof FakeServer>;

// Counter identifying the latest in-flight grand-total request. On arrival each fetch checks its
// captured id against the counter; if it's been superseded by a newer request (e.g. a second
// filter change before the first fetch returned), the stale response is discarded.
let latestGrandTotalRequestId = 0;

const getServerSideDatasource: (
  server: ReturnType<typeof FakeServer>,
) => IServerSideDatasource = (server: ReturnType<typeof FakeServer>) => {
  return {
    getRows: (params) => {
      console.log("[Datasource] - rows requested:", params.request);
      const response = server.getData(params.request, false);
      const needsGrandTotal = params.needsGrandTotal;
      setTimeout(() => {
        if (!response.success) {
          params.fail();
          return;
        }
        // grandTotalData is deliberately omitted here — the async refresh below owns it.
        params.success({
          rowData: response.rows,
          rowCount: response.lastRow,
        });
        // `refreshGrandTotalAsync`'s first act is a `remove` transaction, which sets
        // store.grandTotalData = null. The grid treats null as "explicitly cleared" so
        // `needsGrandTotal` stays false for subsequent block requests in the same store
        // — this branch fires exactly once per logical query.
        if (needsGrandTotal) {
          void refreshGrandTotalAsync(params);
        }
      }, 800);
    },
  };
};

async function refreshGrandTotalAsync(
  params: IServerSideGetRowsParams<OlympicRow>,
) {
  const { api, request } = params;
  const thisRequestId = ++latestGrandTotalRequestId;
  console.log(`[GrandTotal] - request ${thisRequestId} started`);
  // Clear the stale total immediately; we'll add the fresh one back when the fetch resolves.
  api.applyServerSideTransaction({
    remove: [{ id: GRAND_TOTAL_ROW_ID } as any],
  });
  // Simulate a separate, backend call for the grand total.
  const grandTotalData = await new Promise<OlympicRow>((resolve) => {
    setTimeout(() => {
      resolve(fakeServer.getData(request, true).grandTotalData);
    }, 1300);
  });
  if (thisRequestId !== latestGrandTotalRequestId) {
    console.log(
      `[GrandTotal] - request ${thisRequestId} ignored (superseded by ${latestGrandTotalRequestId})`,
    );
    return;
  }
  api.applyServerSideTransaction({ add: [grandTotalData] });
  console.log(`[GrandTotal] - request ${thisRequestId} applied`);
}

const GridExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "athlete", minWidth: 170 },
    { field: "country" },
    { field: "sport" },
    { field: "year", filter: "agNumberColumnFilter", floatingFilter: true },
    // aggFunc on a flat grid has no client-side effect, but the SSRM request's valueCols
    // carries it to the server so our grand-total fetch uses the right aggregation.
    {
      field: "gold",
      aggFunc: "sum",
      filter: "agNumberColumnFilter",
      floatingFilter: true,
    },
    {
      field: "silver",
      aggFunc: "sum",
      filter: "agNumberColumnFilter",
      floatingFilter: true,
    },
    {
      field: "bronze",
      aggFunc: "sum",
      filter: "agNumberColumnFilter",
      floatingFilter: true,
    },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      flex: 1,
      minWidth: 120,
    };
  }, []);
  const getRowId = useCallback(
    (params: GetRowIdParams<OlympicRow>) => params.data.id,
    [],
  );

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: OlympicRow[]) => {
        // Olympic rows aren't unique by athlete/country/year/sport, so a composite natural
        // key collides. Index-based ids guarantee uniqueness.
        const dataWithIds: OlympicRow[] = data.map((row, i) => ({
          ...row,
          id: `row-${i}`,
        }));
        fakeServer = new FakeServer(dataWithIds);
        params.api!.setGridOption(
          "serverSideDatasource",
          getServerSideDatasource(fakeServer),
        );
      });
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div className="example-wrapper">
          <div style={gridStyle}>
            <AgGridReact<OlympicRow>
              columnDefs={columnDefs}
              defaultColDef={defaultColDef}
              rowModelType={"serverSide"}
              grandTotalRow={"bottom"}
              cacheBlockSize={20}
              getRowId={getRowId}
              onGridReady={onGridReady}
            />
          </div>
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Grand Total with Transactions](https://www.ag-grid.com/examples/server-side-model-grouping/grand-total-transactions/reactFunctionalTs)

### Accessing Grand Total and Group Total Rows

Both the grand total and individual group total rows can be retrieved by ID using `api.getRowNode()`. Two constants, exported from `ag-grid-community`, define the ID format:

- `GRAND_TOTAL_ROW_ID` (`'rowGroupFooter_ROOT_NODE_ID'`) — the ID of the grand total row.
- `GROUP_TOTAL_ROW_ID_PREFIX` (`'rowGroupFooter_'`) — the prefix for group total row IDs. A group total row ID is `GROUP_TOTAL_ROW_ID_PREFIX + groupRowNode.id`.

```js
// Retrieve the grand total row node
const grandTotalNode = api.getRowNode(GRAND_TOTAL_ROW_ID);

// Retrieve a group total row node (e.g. for group "Ireland")
const groupTotalNode = api.getRowNode(GROUP_TOTAL_ROW_ID_PREFIX + groupRowNode.id);
```

### Grand Total Row API Reference

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `grandTotalRow` | `'top' \| 'bottom' \| 'pinnedTop' \| 'pinnedBottom'` |  |  | When provided, an extra grand total row will be inserted into the grid at the specified position. This row displays the aggregate totals of all rows in the grid. Modules (any of): [`RowGroupingModule`](https://www.ag-grid.com/react-data-grid/modules/), [`ServerSideRowModelModule`](https://www.ag-grid.com/react-data-grid/modules/). |

## Hide Open Parents

In some configurations it may be desired for the group row to be hidden when expanded, this can be achieved by setting the `groupHideOpenParents` property to true.

The example below has been styled in a way that demonstrates the behaviour of the groups. Note how upon expanding a group, the group row is replaced by the first of its children, and only when collapsed is the group row is shown again.

#### Hide Open Parents

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  ModuleRegistry,
  RowModelType,
  enableDevValidations,
} from "ag-grid-community";
import {
  RowGroupingModule,
  ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [RowGroupingModule, ServerSideRowModelModule];

const getServerSideDatasource: (server: any) => IServerSideDatasource = (
  server: any,
) => {
  return {
    getRows: (params) => {
      console.log("[Datasource] - rows requested by grid: ", params.request);
      const response = server.getData(params.request);
      // adding delay to simulate real server call
      setTimeout(() => {
        if (response.success) {
          // call the success callback
          params.success({
            rowData: response.rows,
            rowCount: response.lastRow,
          });
        } else {
          // inform the grid request failed
          params.fail();
        }
      }, 500);
    },
  };
};

const GridExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "country", rowGroup: true, hide: true },
    { field: "sport", rowGroup: true, hide: true },
    { field: "year", minWidth: 100 },
    { field: "gold", aggFunc: "sum", enableValue: true },
    { field: "silver", aggFunc: "sum", enableValue: true },
    { field: "bronze", aggFunc: "sum", enableValue: true },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      flex: 1,
      minWidth: 120,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      flex: 1,
      minWidth: 280,
    };
  }, []);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: IOlympicData[]) => {
        // setup the fake server with entire dataset
        const fakeServer = new FakeServer(data);
        // create datasource with a reference to the fake server
        const datasource = getServerSideDatasource(fakeServer);
        // register the datasource with the grid
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div className="example-wrapper">
          <div className="example-header">
            <span className="legend-item ag-row-level-0"></span>
            <span className="legend-label">Top Level Group</span>
            <span className="legend-item ag-row-level-1"></span>
            <span className="legend-label">Second Level Group</span>
            <span className="legend-item ag-row-level-2"></span>
            <span className="legend-label">Bottom Rows</span>
          </div>

          <div style={gridStyle}>
            <AgGridReact<IOlympicData>
              columnDefs={columnDefs}
              defaultColDef={defaultColDef}
              autoGroupColumnDef={autoGroupColumnDef}
              rowModelType={"serverSide"}
              groupHideOpenParents={true}
              cacheBlockSize={5}
              onGridReady={onGridReady}
            />
          </div>
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Hide Open Parents](https://www.ag-grid.com/examples/server-side-model-grouping/hide-open-parents/reactFunctionalTs)

> **Note**
>
> When `groupHideOpenParents=true` the Grid automatically disables the [Sticky Groups](https://www.ag-grid.com/react-data-grid/grouping-opening-groups/#prevent-sticky-groups) behaviour of the rows as well as [Full Width Loading](https://www.ag-grid.com/react-data-grid/component-loading-cell-renderer/#skeleton-loading).

## Unbalanced Groups

To enable unbalanced groups in the SSRM, set the `groupAllowUnbalanced` property to true. This causes any group with a key of `''` to behave as if it is always expanded, and the group row to always be hidden.

#### Unbalanced Groups

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  ModuleRegistry,
  RowModelType,
  enableDevValidations,
} from "ag-grid-community";
import {
  RowGroupingModule,
  ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [RowGroupingModule, ServerSideRowModelModule];

const getServerSideDatasource: (server: any) => IServerSideDatasource = (
  server: any,
) => {
  return {
    getRows: (params) => {
      console.log("[Datasource] - rows requested by grid: ", params.request);
      const response = server.getData(params.request);
      // adding delay to simulate real server call
      setTimeout(() => {
        if (response.success) {
          // call the success callback
          params.success({
            rowData: response.rows,
            rowCount: response.lastRow,
          });
        } else {
          // inform the grid request failed
          params.fail();
        }
      }, 2000);
    },
  };
};

const GridExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "country", rowGroup: true, hide: true },
    { field: "sport" },
    { field: "year", minWidth: 100 },
    { field: "gold", aggFunc: "sum", enableValue: true },
    { field: "silver", aggFunc: "sum", enableValue: true },
    { field: "bronze", aggFunc: "sum", enableValue: true },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      flex: 1,
      minWidth: 120,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      flex: 1,
      minWidth: 280,
    };
  }, []);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/small-olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: IOlympicData[]) => {
        // add unbalanced data to the top of the dataset
        const unbalancedData = data.map((item: IOlympicData) => ({
          ...item,
          country: item.country === null ? "" : item.country,
        }));
        unbalancedData.sort((a: IOlympicData, b: IOlympicData) =>
          a.country === "" ? -1 : 1,
        );
        // setup the fake server with entire dataset
        const fakeServer = new FakeServer(unbalancedData);
        // create datasource with a reference to the fake server
        const datasource = getServerSideDatasource(fakeServer);
        // register the datasource with the grid
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div style={gridStyle}>
          <AgGridReact<IOlympicData>
            columnDefs={columnDefs}
            defaultColDef={defaultColDef}
            autoGroupColumnDef={autoGroupColumnDef}
            rowModelType={"serverSide"}
            groupAllowUnbalanced={true}
            onGridReady={onGridReady}
          />
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Unbalanced Groups](https://www.ag-grid.com/examples/server-side-model-grouping/unbalanced-groups/reactFunctionalTs)

> **Note**
>
> When using `groupAllowUnbalanced=true` it is important to remember that a row group still exists to contain the unbalanced nodes, this can be an important consideration when working with selection state, refreshing, or group paths. This also means that there will be additional requests and delays in loading these unbalanced rows, as they do not belong to the parent row.

## Expand All / Collapse All

Group rows can be expanded or collapsed using the `expandAll()` and `collapseAll()` grid API's. By default, these operations apply only to **loaded group rows** (not all groups). To expand/collapse **all groups**, including those not yet loaded, set `ssrmExpandAllAffectsAllRows: true` in your grid options.

The example below demonstrates this feature, note the following:

- First button expands all loaded group rows
- Checking the checkbox enables `ssrmExpandAllAffectsAllRows` in the grid options
- Now clicking the first button expands all group rows, including those not yet loaded
- Second button collapses all group rows

#### expand-all-affects-all-rows

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
  ColDef,
  ColGroupDef,
  GetRowIdFunc,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  ModuleRegistry,
  RowModelType,
  enableDevValidations,
} from "ag-grid-community";
import {
  RowGroupingModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [
  RowGroupingModule,
  ServerSideRowModelModule,
  ServerSideRowModelApiModule,
];

const getServerSideDatasource: (server: any) => IServerSideDatasource = (
  server: any,
) => {
  return {
    getRows: (params) => {
      const response = server.getData(params.request);
      // adding delay to simulate real server call
      setTimeout(() => {
        if (response.success) {
          // call the success callback
          params.success({
            rowData: response.rows,
            rowCount: response.lastRow,
          });
        } else {
          // inform the grid request failed
          params.fail();
        }
      }, 100);
    },
  };
};

const GridExample = () => {
  const gridRef = useRef<AgGridReact>(null);
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "90vh", width: "100%" }), []);

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "country", rowGroup: true, hide: true },
    { field: "id", aggFunc: "sum", hide: true },
    { field: "sport", rowGroup: true, hide: true },
    { field: "year", rowGroup: true, hide: true },
    { field: "gold", aggFunc: "sum" },
    { field: "silver", aggFunc: "sum" },
    { field: "bronze", aggFunc: "sum" },
  ]);
  const getRowId = useCallback((params) => {
    const parentKeysJoined = (params.parentKeys || []).join("-");
    if (params.data.id != null) {
      return parentKeysJoined + params.data.id;
    }
    const rowGroupCols = params.api.getRowGroupColumns();
    const thisGroupCol = rowGroupCols[params.level];
    return parentKeysJoined + params.data[thisGroupCol.getColDef().field!];
  }, []);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: any[]) => {
        const newData = data.map((e: IOlympicData, i: number) => ({
          ...e,
          id: i,
        }));
        // setup the fake server with entire dataset
        const fakeServer = FakeServer(newData);
        // create datasource with a reference to the fake server
        const datasource = getServerSideDatasource(fakeServer);
        // register the datasource with the grid
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  const onExpandAll = useCallback(() => {
    gridRef.current!.api.expandAll();
  }, []);

  const onCollapseAll = useCallback(() => {
    gridRef.current!.api.collapseAll();
  }, []);

  const onOptionChange = useCallback(() => {
    const ssrmExpandAllAffectsAllRows =
      document.querySelector<HTMLInputElement>(
        "#ssrmExpandAllAffectsAllRows",
      )!.checked;
    gridRef.current!.api.setGridOption(
      "ssrmExpandAllAffectsAllRows",
      ssrmExpandAllAffectsAllRows,
    );
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div className="example-wrapper" style={{ height: "100vh" }}>
          <div className="example-header" style={{ height: "10vh" }}>
            <button id="expand" onClick={onExpandAll}>
              Expand rows
            </button>
            <button id="collapse" onClick={onCollapseAll}>
              Collapse rows
            </button>
            <label>
              ssrmExpandAllAffectsAllRows:
              <input
                type="checkbox"
                id="ssrmExpandAllAffectsAllRows"
                onChange={onOptionChange}
              />
            </label>
          </div>

          <div style={gridStyle}>
            <AgGridReact
              ref={gridRef}
              columnDefs={columnDefs}
              getRowId={getRowId}
              rowModelType={"serverSide"}
              purgeClosedRowNodes={true}
              onGridReady={onGridReady}
            />
          </div>
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: expand-all-affects-all-rows](https://www.ag-grid.com/examples/server-side-model-grouping/expand-all-affects-all-rows/reactFunctionalTs)

To open only specific groups, e.g. only groups at the top level, then use the `forEachNode()` callback and open / close the row using `setExpanded()` as follows:

```jsx
// Expand all top level row nodes
gridApi.forEachNode(node => {
    if (node.group && node.level == 0) {
        node.setExpanded(true);
    }
});
```

The example below demonstrates these techniques. Note the following:

- Clicking 'Expand All' expands all loaded group rows. Doing this when the grid initially loads expands all Year groups. Clicking it a second time (after Year groups have loaded) causes all Year groups as well as their children Country groups to be expanded - this is a heavier operation with 100's of rows to expand.
- Clicking 'Collapse All' collapses all rows.
- Clicking 'Expand Top Level Only' expands Years only, even if more group rows are loaded.

#### Expand All

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  ModuleRegistry,
  RowApiModule,
  RowModelType,
  enableDevValidations,
} from "ag-grid-community";
import {
  RowGroupingModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [
  RowApiModule,
  RowGroupingModule,
  ServerSideRowModelModule,
  ServerSideRowModelApiModule,
];

const getServerSideDatasource: (server: any) => IServerSideDatasource = (
  server: any,
) => {
  return {
    getRows: (params: IServerSideGetRowsParams) => {
      console.log("[Datasource] - rows requested by grid: ", params.request);
      const response = server.getData(params.request);
      // adding delay to simulate real server call
      setTimeout(() => {
        if (response.success) {
          // call the success callback
          params.success({
            rowData: response.rows,
            rowCount: response.lastRow,
            groupLevelInfo: {
              lastLoadedTime: new Date().toLocaleString(),
              randomValue: window.agRandom(),
            },
          });
        } else {
          // inform the grid request failed
          params.fail();
        }
      }, 200);
    },
  };
};

const GridExample = () => {
  const gridRef = useRef<AgGridReact<IOlympicData>>(null);
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    {
      field: "year",
      enableRowGroup: true,
      rowGroup: true,
      hide: true,
      minWidth: 100,
    },
    { field: "country", enableRowGroup: true, rowGroup: true, hide: true },
    { field: "sport", enableRowGroup: true, rowGroup: true, hide: true },
    { field: "gold", aggFunc: "sum", enableValue: true },
    { field: "silver", aggFunc: "sum", enableValue: true },
    { field: "bronze", aggFunc: "sum", enableValue: true },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      flex: 1,
      minWidth: 120,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      flex: 1,
      minWidth: 280,
    };
  }, []);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: IOlympicData[]) => {
        // setup the fake server with entire dataset
        const fakeServer = new FakeServer(data);
        // create datasource with a reference to the fake server
        const datasource = getServerSideDatasource(fakeServer);
        // register the datasource with the grid
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  const onBtExpandAll = useCallback(() => {
    gridRef.current!.api.expandAll();
  }, []);

  const onBtCollapseAll = useCallback(() => {
    gridRef.current!.api.collapseAll();
  }, []);

  const onBtExpandTopLevel = useCallback(() => {
    gridRef.current!.api.forEachNode(function (node) {
      if (node.group && node.level == 0) {
        node.setExpanded(true);
      }
    });
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div className="example-wrapper">
          <div style={{ marginBottom: "5px" }}>
            <button onClick={onBtExpandAll}>Expand All</button>
            &nbsp;&nbsp;&nbsp;
            <button onClick={onBtCollapseAll}>Collapse All</button>
            &nbsp;&nbsp;&nbsp;
            <button onClick={onBtExpandTopLevel}>Expand Top Level Only</button>
          </div>

          <div style={gridStyle}>
            <AgGridReact<IOlympicData>
              ref={gridRef}
              columnDefs={columnDefs}
              defaultColDef={defaultColDef}
              autoGroupColumnDef={autoGroupColumnDef}
              maxConcurrentDatasourceRequests={1}
              rowModelType={"serverSide"}
              onGridReady={onGridReady}
            />
          </div>
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Expand All](https://www.ag-grid.com/examples/server-side-model-grouping/expand-all/reactFunctionalTs)

## Providing Child Counts

By default, the grid does not show row counts beside the group names. If you do want row counts, you need to implement the `getChildCount(dataItem)` callback for the grid. The callback provides you with the row data; it is your application's responsibility to know what the child row count is. The suggestion is you set this information into the row data item you provide to the grid.

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `getChildCount` | `GetChildCount` |  |  | Allows setting the child count for a group row. Module: [`ServerSideRowModelModule`](https://www.ag-grid.com/react-data-grid/modules/). [Initial](https://www.ag-grid.com/react-data-grid/grid-interface/#initial-grid-options). |

```jsx
const getChildCount = data => {
    // here child count is stored in the 'childCount' property
    return data.childCount;
};

<AgGridReact getChildCount={getChildCount} />
```

#### Child Counts

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GetChildCount,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  ModuleRegistry,
  RowModelType,
  enableDevValidations,
} from "ag-grid-community";
import {
  RowGroupingModule,
  ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [RowGroupingModule, ServerSideRowModelModule];

const getServerSideDatasource: (server: any) => IServerSideDatasource = (
  server: any,
) => {
  return {
    getRows: (params) => {
      console.log("[Datasource] - rows requested by grid: ", params.request);
      const response = server.getData(params.request);
      // adding delay to simulate real server call
      setTimeout(() => {
        if (response.success) {
          // call the success callback
          params.success({
            rowData: response.rows,
            rowCount: response.lastRow,
          });
        } else {
          // inform the grid request failed
          params.fail();
        }
      }, 200);
    },
  };
};

const GridExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "country", rowGroup: true, hide: true },
    { field: "sport", rowGroup: true, hide: true },
    { field: "gold", aggFunc: "sum", enableValue: true },
    { field: "silver", aggFunc: "sum", enableValue: true },
    { field: "bronze", aggFunc: "sum", enableValue: true },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      flex: 1,
      minWidth: 150,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      flex: 1,
      minWidth: 280,
    };
  }, []);
  const getChildCount = useCallback((data: any) => {
    return data ? data.childCount : undefined;
  }, []);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: IOlympicData[]) => {
        // setup the fake server with entire dataset
        const fakeServer = new FakeServer(data);
        // create datasource with a reference to the fake server
        const datasource = getServerSideDatasource(fakeServer);
        // register the datasource with the grid
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div style={gridStyle}>
          <AgGridReact<IOlympicData>
            columnDefs={columnDefs}
            defaultColDef={defaultColDef}
            autoGroupColumnDef={autoGroupColumnDef}
            rowModelType={"serverSide"}
            getChildCount={getChildCount}
            onGridReady={onGridReady}
          />
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Child Counts](https://www.ag-grid.com/examples/server-side-model-grouping/child-counts/reactFunctionalTs)

## Group via Value Getter

It is possible the data provided has composite objects, in which case it's more difficult for the grid to extract group names. This can be worked with using value getters or embedded fields (i.e. the field attribute has dot notation).

In the example below, all rows are modified so that the rows look something like this:

```js
// sample contents of row data
const rowData = {
    // country field is complex object
    country: {
        name: 'Ireland',
        code: 'IRE'
    },

    // other fields as normal
    ...
}
```

Then the columns are set up so that country uses a `valueGetter` that uses the field with dot notation, i.e. `data.country.name`

#### Complex Objects

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  ModuleRegistry,
  RowModelType,
  enableDevValidations,
} from "ag-grid-community";
import {
  RowGroupingModule,
  ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [RowGroupingModule, ServerSideRowModelModule];

const getServerSideDatasource: (server: any) => IServerSideDatasource = (
  server: any,
) => {
  return {
    getRows: (params) => {
      console.log("[Datasource] - rows requested by grid: ", params.request);
      const response = server.getData(params.request);
      // convert country to a complex object
      const resultsWithComplexObjects = response.rows.map(function (row: any) {
        row.country = {
          name: row.country,
          code: row.country.substring(0, 3).toUpperCase(),
        };
        return row;
      });
      // adding delay to simulate real server call
      setTimeout(() => {
        if (response.success) {
          // call the success callback
          params.success({
            rowData: resultsWithComplexObjects,
            rowCount: response.lastRow,
          });
        } else {
          // inform the grid request failed
          params.fail();
        }
      }, 200);
    },
  };
};

const GridExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    // here we are using a valueGetter to get the country name from the complex object
    {
      colId: "country",
      valueGetter: "data.country.name",
      rowGroup: true,
      hide: true,
    },
    { field: "gold", aggFunc: "sum", enableValue: true },
    { field: "silver", aggFunc: "sum", enableValue: true },
    { field: "bronze", aggFunc: "sum", enableValue: true },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      flex: 1,
      minWidth: 150,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      flex: 1,
      minWidth: 280,
    };
  }, []);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: IOlympicData[]) => {
        // setup the fake server with entire dataset
        const fakeServer = new FakeServer(data);
        // create datasource with a reference to the fake server
        const datasource = getServerSideDatasource(fakeServer);
        // register the datasource with the grid
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div style={gridStyle}>
          <AgGridReact<IOlympicData>
            columnDefs={columnDefs}
            defaultColDef={defaultColDef}
            autoGroupColumnDef={autoGroupColumnDef}
            rowModelType={"serverSide"}
            onGridReady={onGridReady}
          />
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Complex Objects](https://www.ag-grid.com/examples/server-side-model-grouping/complex-objects/reactFunctionalTs)

## Filters

By default, changing filters fully purges the grid. Though, it can be configured to only refresh when the group has been directly impacted by enabling `serverSideOnlyRefreshFilteredGroups`. Be aware, this can mean your grid may have empty group rows. This is because the grid does not refresh the groups above the groups it deems impacted by the filter.

In the example below, note the following:

- Filtering by `Gold`, `Silver` or `Bronze` fully purges the grid, this is because they have aggregations applied.
- Applying a filter to the `Year` column does not purge the entire grid, and instead only refreshes the `Year` group rows.
- The example enables `serverSideOnlyRefreshFilteredGroups`, note that if you apply a filter to `Year` with the value `1900`, no leaf rows exist in any group.

#### Filtering

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  ModuleRegistry,
  NumberFilterModule,
  RowModelType,
  TextFilterModule,
  enableDevValidations,
} from "ag-grid-community";
import {
  RowGroupingModule,
  ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";
import { IOlympicData } from "./interfaces";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [
  TextFilterModule,
  RowGroupingModule,
  ServerSideRowModelModule,
  NumberFilterModule,
];

const getServerSideDatasource: (server: any) => IServerSideDatasource = (
  server: any,
) => {
  return {
    getRows: (params) => {
      console.log("[Datasource] - rows requested by grid: ", params.request);
      const response = server.getData(params.request);
      // adding delay to simulate real server call
      setTimeout(() => {
        if (response.success) {
          // call the success callback
          params.success({
            rowData: response.rows,
            rowCount: response.lastRow,
          });
        } else {
          // inform the grid request failed
          params.fail();
        }
      }, 1000);
    },
  };
};

const GridExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "country", rowGroup: true, hide: true },
    { field: "sport", rowGroup: true, hide: true },
    {
      field: "year",
      minWidth: 100,
      filter: "agNumberColumnFilter",
      floatingFilter: true,
    },
    {
      field: "gold",
      aggFunc: "sum",
      filter: "agNumberColumnFilter",
      floatingFilter: true,
      enableValue: true,
    },
    {
      field: "silver",
      aggFunc: "sum",
      filter: "agNumberColumnFilter",
      floatingFilter: true,
      enableValue: true,
    },
    {
      field: "bronze",
      aggFunc: "sum",
      filter: "agNumberColumnFilter",
      floatingFilter: true,
      enableValue: true,
    },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      flex: 1,
      minWidth: 120,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      flex: 1,
      minWidth: 280,
      field: "athlete",
    };
  }, []);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: IOlympicData[]) => {
        // setup the fake server with entire dataset
        const fakeServer = new FakeServer(data);
        // create datasource with a reference to the fake server
        const datasource = getServerSideDatasource(fakeServer);
        // register the datasource with the grid
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div style={gridStyle}>
          <AgGridReact<IOlympicData>
            columnDefs={columnDefs}
            defaultColDef={defaultColDef}
            autoGroupColumnDef={autoGroupColumnDef}
            serverSideOnlyRefreshFilteredGroups={true}
            rowModelType={"serverSide"}
            cacheBlockSize={5}
            onGridReady={onGridReady}
          />
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Filtering](https://www.ag-grid.com/examples/server-side-model-grouping/filtering/reactFunctionalTs)

## Deferred Column Configuration

You can configure the Columns Tool Panel to stage changes and require an explicit **Apply** action before they are committed. This allows multiple configuration changes to be made and applied in a single update, avoiding unnecessary intermediate recomputations or requests.

Deferred Updates are enabled by including the **Apply** button in `toolPanelParams.buttons`.

Note that in the example below:

- Changes made in the Columns Tool Panel are staged as pending changes.
- **Apply** commits all pending changes in a single operation.
- **Cancel** discards all pending changes and restores the last applied state.

#### Deferred Updates

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
  AutoGroupColumnDef,
  ColDef,
  ColGroupDef,
  GetChildCount,
  GridApi,
  GridOptions,
  GridReadyEvent,
  ModuleRegistry,
  RowModelType,
  SideBarDef,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  PivotModule,
  RowGroupingPanelModule,
  ServerSideRowModelModule,
} from "ag-grid-enterprise";
import { createFakeServer, createServerSideDatasource } from "./fakeServer";
import { IOlympicData } from "./interfaces";

if (process.env.NODE_ENV !== "production") {
  enableDevValidations();
}

const modules = [
  ColumnsToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  PivotModule,
  RowGroupingPanelModule,
  ServerSideRowModelModule,
];

const GridExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    {
      field: "athlete",
      minWidth: 200,
      enableRowGroup: true,
      enablePivot: true,
    },
    {
      field: "age",
      enableValue: true,
    },
    {
      field: "country",
      minWidth: 200,
      enableRowGroup: true,
      enablePivot: true,
      rowGroupIndex: 1,
    },
    {
      field: "year",
      enableRowGroup: true,
      enablePivot: true,
    },
    {
      field: "date",
      minWidth: 180,
      enableRowGroup: true,
      enablePivot: true,
    },
    {
      field: "sport",
      minWidth: 200,
      enableRowGroup: true,
      enablePivot: true,
      rowGroupIndex: 2,
    },
    { field: "gold", hide: true, enableValue: true },
    { field: "silver", hide: true, enableValue: true, aggFunc: "sum" },
    { field: "bronze", hide: true, enableValue: true, aggFunc: "sum" },
    { headerName: "Total", field: "total", enableValue: true },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      flex: 1,
      minWidth: 150,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      minWidth: 250,
    };
  }, []);
  const sideBar = useMemo<
    SideBarDef | string | string[] | boolean | null
  >(() => {
    return {
      toolPanels: [
        {
          id: "columns",
          labelDefault: "Columns",
          labelKey: "columns",
          iconKey: "columns",
          toolPanel: "agColumnsToolPanel",
          toolPanelParams: {
            buttons: ["cancel", "apply"],
          },
        },
      ],
      defaultToolPanel: "columns",
    };
  }, []);
  const getChildCount = useCallback(
    (data: any) =>
      typeof data?.childCount === "number" ? data.childCount : undefined,
    [],
  );

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
      .then((resp) => resp.json())
      .then((data: IOlympicData[]) => {
        const fakeServer = createFakeServer(data);
        const datasource = createServerSideDatasource(fakeServer);
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div className="example-wrapper">
          <div style={gridStyle}>
            <AgGridReact<IOlympicData>
              columnDefs={columnDefs}
              defaultColDef={defaultColDef}
              autoGroupColumnDef={autoGroupColumnDef}
              rowModelType={"serverSide"}
              rowGroupPanelShow={"always"}
              pivotPanelShow={"always"}
              sideBar={sideBar}
              getChildCount={getChildCount}
              onGridReady={onGridReady}
            />
          </div>
        </div>
      </div>
    </AgGridProvider>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <GridExample />
  </StrictMode>,
);
```

[Live example: Deferred Updates](https://www.ag-grid.com/examples/server-side-model-grouping/deferred-apply-mode/reactFunctionalTs)

```jsx
const sideBar = useMemo(() => { 
	return {
        toolPanels: [
            {
                id: 'columns',
                labelDefault: 'Columns',
                labelKey: 'columns',
                iconKey: 'columns',
                toolPanel: 'agColumnsToolPanel',
                toolPanelParams: {
                    buttons: ['cancel', 'apply'],
                },
            },
        ],
        defaultToolPanel: 'columns',
    };
}, []);

<AgGridReact sideBar={sideBar} />
```

> **Note**
>
> Changes made outside the Columns Tool Panel — such as dragging columns into the Row Group or Pivot Panels, using the Column Menu, or calling the Grid / Column API — are applied immediately and clear any pending changes. Column pinning, resizing, and group expansion do not clear pending changes.
