---
title: "SSRM Tree Data"
enterprise: true
framework: react
version: "36.1.0"
---

# SSRM Tree Data

This section shows how Tree Data can be used with the Server-Side Row Model.

## Tree Data

Tree Data is defined as data that has parent / child relationships where the parent / child relationships are provided as part of the data. This is in contrast to Row Grouping where the parent / child relationships are the result of grouping. When working with Tree Data, there are no columns getting grouped.

An example of a Tree Data JSON structure is shown below:

```json
[{
    "employeeId": 101,
    "employeeName": "Erica Rogers",
    "jobTitle": "CEO",
    "employmentType": "Permanent",
    "children": [{
        "employeeId": 102,
        "employeeName": "Malcolm Barrett",
        "jobTitle": "Exec. Vice President",
        "employmentType": "Permanent",
        "children": [
            {
                "employeeId": 103,
                "employeeName": "Leah Flowers",
                "jobTitle": "Parts Technician",
                "employmentType": "Contract"
            },
            {
                "employeeId": 104,
                "employeeName": "Tammy Sutton",
                "jobTitle": "Service Technician",
                "employmentType": "Contract"
            }
        ]
    }]
}]
```

It is expected that the data set will be too large to send over the network, hence the SSRM is used to lazy-load child row as the parent rows are expanded.

## Tree Data Mode

In order to set the grid to work with Tree Data, simply enable Tree Data mode via the Grid Options using: `gridOptions.treeData = true`.

## Supplying Tree Data

Tree Data is supplied via the [Server-Side Datasource](https://www.ag-grid.com/react-data-grid/server-side-model-datasource/) just like flat data, however there are two additional gridOptions callbacks: `isServerSideGroup(dataItem)` and `getServerSideGroupKey(dataItem)`.

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `isServerSideGroup` | `IsServerSideGroup` |  |  | SSRM Tree Data: Allows specifying which rows are expandable. Module: [`ServerSideRowModelModule`](https://www.ag-grid.com/react-data-grid/modules/). |
| `getServerSideGroupKey` | `GetServerSideGroupKey` |  |  | SSRM Tree Data: Allows specifying group keys. Module: [`ServerSideRowModelModule`](https://www.ag-grid.com/react-data-grid/modules/). |

The following code snippet shows the relevant `gridOptions` entries for configuring tree data with the server-side row model:

```jsx
// choose Server-Side Row Model
const rowModelType = 'serverSide';
// enable Tree Data
const treeData = true;
// indicate if row is a group node
const isServerSideGroup = dataItem => {
    return dataItem.group;
};
// specify which group key to use
const getServerSideGroupKey = dataItem => {
    return dataItem.employeeId;
};

<AgGridReact
    rowModelType={rowModelType}
    treeData={treeData}
    isServerSideGroup={isServerSideGroup}
    getServerSideGroupKey={getServerSideGroupKey}
/>
```

> **Note**
>
> Be careful not to get mixed up with the [Client-Side Tree Data](https://www.ag-grid.com/react-data-grid/tree-data/) configurations by mistake.

The example below shows this in action where the following can be noted:

- Tree Data is enabled with the Server-Side Row Model using `gridOptions.treeData = true`.
- Group nodes are determined using the callback `gridOptions.isServerSideGroup(dataItem)`.
- Group keys are returned from the callback `gridOptions.getServerSideGroupKey(dataItem)`.

#### Tree Data

```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,
  GetServerSideGroupKey,
  GridApi,
  GridOptions,
  GridReadyEvent,
  ICellRendererParams,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IServerSideGetRowsRequest,
  IsServerSideGroup,
  IsServerSideGroupOpenByDefault,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  RowModelType,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";

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

const modules = [
  ColumnsToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  TreeDataModule,
  ServerSideRowModelModule,
];

function createFakeServer(fakeServerData: any[]) {
  const fakeServer = {
    data: fakeServerData,
    getData: function (request: IServerSideGetRowsRequest) {
      const extractRowsFromData: (groupKeys: string[], data: any[]) => any = (
        groupKeys: string[],
        data: any[],
      ) => {
        if (groupKeys.length === 0) {
          return data.map(function (d) {
            return {
              group: !!d.children,
              employeeId: d.employeeId,
              employeeName: d.employeeName,
              employmentType: d.employmentType,
              jobTitle: d.jobTitle,
            };
          });
        }
        const key = groupKeys[0];
        for (let i = 0; i < data.length; i++) {
          if (data[i].employeeId === key) {
            return extractRowsFromData(
              groupKeys.slice(1),
              data[i].children.slice(),
            );
          }
        }
      };
      return extractRowsFromData(request.groupKeys, this.data);
    },
  };
  return fakeServer;
}

function createServerSideDatasource(fakeServer: any) {
  const dataSource: IServerSideDatasource = {
    getRows: (params: IServerSideGetRowsParams) => {
      console.log("ServerSideDatasource.getRows: params = ", params);
      const allRows = fakeServer.getData(params.request);
      const request = params.request;
      const doingInfinite = request.startRow != null && request.endRow != null;
      const result = doingInfinite
        ? {
            rowData: allRows.slice(request.startRow, request.endRow),
            rowCount: allRows.length,
          }
        : { rowData: allRows };
      console.log("getRows: result = ", result);
      setTimeout(() => {
        params.success(result);
      }, 200);
    },
  };
  return dataSource;
}

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

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "employeeId", hide: true },
    { field: "employeeName", hide: true },
    { field: "jobTitle" },
    { field: "employmentType" },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      width: 240,
      flex: 1,
      sortable: false,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      field: "employeeName",
      cellRendererParams: {
        innerRenderer: (params: ICellRendererParams) => {
          // display employeeName rather than group key (employeeId)
          return params.data.employeeName;
        },
      },
    };
  }, []);
  const isServerSideGroupOpenByDefault = useCallback(
    (params: IsServerSideGroupOpenByDefaultParams) => {
      // open first two levels by default
      return params.rowNode.level < 2;
    },
    [],
  );
  const isServerSideGroup = useCallback((dataItem: any) => {
    // indicate if node is a group
    return dataItem.group;
  }, []);
  const getServerSideGroupKey = useCallback((dataItem: any) => {
    // specify which group key to use
    return dataItem.employeeId;
  }, []);

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

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

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

[Live example: Tree Data](https://www.ag-grid.com/examples/server-side-model-tree-data/tree-data/reactFunctionalTs)

## Refreshing Tree Data

Tree Data can be refreshed in the same way as groups are refreshed when not using Tree Data. This is explained in the [SSRM Refresh](https://www.ag-grid.com/react-data-grid/server-side-model-updating-refresh/).

The example below shows this in action where the following can be noted:

- Click **Refresh Everything** to clear all caches by calling `api.refreshServerSide({ route: [], purge: true })`.
- Click **Refresh ['Kathryn Powers','Mabel Ward']** to clear a single cache by calling `api.refreshServerSide({ route: ['Kathryn Powers','Mabel Ward'], purge: true })`.

#### Purging Tree Data

```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,
  GetServerSideGroupKey,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IServerSideGetRowsRequest,
  IsServerSideGroup,
  IsServerSideGroupOpenByDefault,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  RowModelType,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";

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

const modules = [
  ColumnsToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  TreeDataModule,
  ServerSideRowModelModule,
  ServerSideRowModelApiModule,
];

function createFakeServer(fakeServerData: any[]) {
  const fakeServer = {
    getData: (request: IServerSideGetRowsRequest) => {
      const extractRowsFromData: (groupKeys: string[], data: any[]) => any = (
        groupKeys: string[],
        data: any[],
      ) => {
        if (groupKeys.length === 0) {
          return data.map(function (d) {
            return {
              group: !!d.underlings,
              employeeId: d.employeeId + "",
              employeeName: d.employeeName,
              employmentType: d.employmentType,
              startDate: d.startDate,
            };
          });
        }
        const key = groupKeys[0];
        for (let i = 0; i < data.length; i++) {
          if (data[i].employeeName === key) {
            return extractRowsFromData(
              groupKeys.slice(1),
              data[i].underlings.slice(),
            );
          }
        }
      };
      return extractRowsFromData(request.groupKeys, fakeServerData);
    },
  };
  return fakeServer;
}

function createServerSideDatasource(fakeServer: any) {
  const dataSource: IServerSideDatasource = {
    getRows: (params: IServerSideGetRowsParams) => {
      console.log("ServerSideDatasource.getRows: params = ", params);
      const request = params.request;
      const allRows = fakeServer.getData(request);
      const doingInfinite = request.startRow != null && request.endRow != null;
      const result = doingInfinite
        ? {
            rowData: allRows.slice(request.startRow, request.endRow),
            rowCount: allRows.length,
          }
        : { rowData: allRows };
      console.log("getRows: result = ", result);
      setTimeout(() => {
        params.success(result);
      }, 500);
    },
  };
  return dataSource;
}

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

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "employeeId", hide: true },
    { field: "employeeName", hide: true },
    { field: "employmentType" },
    { field: "startDate" },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      width: 235,
      flex: 1,
      sortable: false,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      field: "employeeName",
    };
  }, []);
  const isServerSideGroupOpenByDefault = useCallback(
    (params: IsServerSideGroupOpenByDefaultParams) => {
      const isKathrynPowers =
        params.rowNode.level == 0 &&
        params.data.employeeName == "Kathryn Powers";
      const isMabelWard =
        params.rowNode.level == 1 && params.data.employeeName == "Mabel Ward";
      return isKathrynPowers || isMabelWard;
    },
    [],
  );
  const isServerSideGroup = useCallback((dataItem: any) => {
    // indicate if node is a group
    return dataItem.group;
  }, []);
  const getServerSideGroupKey = useCallback((dataItem: any) => {
    // specify which group key to use
    return dataItem.employeeName;
  }, []);

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

  const refreshCache = useCallback((route: string[]) => {
    gridRef.current!.api.refreshServerSide({ route: route, purge: true });
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div className="example-wrapper">
          <div style={{ marginBottom: "5px" }}>
            <button onClick={() => refreshCache([])}>Refresh Everything</button>
            <button
              onClick={() => refreshCache(["Kathryn Powers", "Mabel Ward"])}
            >
              Refresh ['Kathryn Powers','Mabel Ward']
            </button>
          </div>

          <div style={gridStyle}>
            <AgGridReact
              ref={gridRef}
              columnDefs={columnDefs}
              defaultColDef={defaultColDef}
              autoGroupColumnDef={autoGroupColumnDef}
              rowModelType={"serverSide"}
              treeData={true}
              cacheBlockSize={10}
              isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
              isServerSideGroup={isServerSideGroup}
              getServerSideGroupKey={getServerSideGroupKey}
              onGridReady={onGridReady}
            />
          </div>
        </div>
      </div>
    </AgGridProvider>
  );
};

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

[Live example: Purging Tree Data](https://www.ag-grid.com/examples/server-side-model-tree-data/purging-tree-data/reactFunctionalTs)

## Transactions with Tree Data

Tree Data can have transactions applied in the same way as row groups. This is explained in the [SSRM Transactions](https://www.ag-grid.com/react-data-grid/server-side-model-updating-transactions/) section.

The example below demonstrates transactions with Tree Data. Note the following:

- **Add Child to Selected** adds a child under the selected row, even if it wasn't originally a group.
- **Update Selected** changes the selected row's `Employment Type`.
- **Delete Selected** removes the selected row, and all of its child rows.
- **Move Selected to Robert Peterson** moves the selected row and its children directly under `Robert Peterson`.

#### Transactions with Tree Data

```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,
  GetServerSideGroupKey,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IRowNode,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IServerSideGetRowsRequest,
  IsServerSideGroup,
  IsServerSideGroupOpenByDefault,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  RowModelType,
  RowSelectionOptions,
  TextFilterModule,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";

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

const modules = [
  TextFilterModule,
  ColumnsToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  TreeDataModule,
  ServerSideRowModelModule,
  ServerSideRowModelApiModule,
];

let fakeServer: {
  getData: (request: IServerSideGetRowsRequest) => void;
  addChildRow: (route: string[], newRow: any) => void;
  toggleEmployment: (route: string[]) => void;
  removeEmployee: (route: string[]) => void;
  moveEmployee: (from: string[], to: string[]) => void;
};

const getRouteToNode: (rowNode: IRowNode) => string[] = (rowNode: IRowNode) => {
  if (!rowNode.parent) {
    return [];
  }
  return [
    ...getRouteToNode(rowNode.parent),
    rowNode.key ? rowNode.key : rowNode.data.employeeName,
  ];
};

let latestId = 100000;

function createFakeServer(fakeServerData: any[], api: GridApi) {
  const getDataAtRoute = (route: string[]) => {
    let mutableRoute = [...route];
    let target: any = { underlings: fakeServerData };
    while (mutableRoute.length) {
      const nextRoute = mutableRoute[0];
      mutableRoute = mutableRoute.slice(1);
      target = target.underlings.find((e: any) => e.employeeName === nextRoute);
    }
    return target;
  };
  const sanitizeRowForGrid = (d: any) => {
    return {
      group: !!d.underlings && !!d.underlings.length,
      employeeId: d.employeeId,
      employeeName: d.employeeName,
      employmentType: d.employmentType,
      startDate: d.startDate,
    };
  };
  fakeServer = {
    getData: (request: IServerSideGetRowsRequest) => {
      const extractRowsFromData: (groupKeys: string[], data: any[]) => any = (
        groupKeys: string[],
        data: any[],
      ) => {
        if (groupKeys.length === 0) {
          return data.map(sanitizeRowForGrid);
        }
        const key = groupKeys[0];
        for (let i = 0; i < data.length; i++) {
          if (data[i].employeeName === key) {
            return extractRowsFromData(
              groupKeys.slice(1),
              data[i].underlings.slice(),
            );
          }
        }
      };
      return extractRowsFromData(request.groupKeys, fakeServerData);
    },
    addChildRow: (route: string[], newRow: any) => {
      const target = getDataAtRoute(route);
      if (!target.underlings || target.underlings.length === 0) {
        target.underlings = [newRow];
        // update the parent row via transaction
        api.applyServerSideTransaction({
          route: route.slice(0, route.length - 1),
          update: [sanitizeRowForGrid(target)],
        });
      } else {
        target.underlings.push(newRow);
        // add the child row via transaction
        api.applyServerSideTransaction({
          route,
          add: [sanitizeRowForGrid(newRow)],
        });
      }
    },
    toggleEmployment: (route: string[]) => {
      const target = getDataAtRoute(route);
      // update the data at the source
      target.employmentType =
        target.employmentType === "Contract" ? "Permanent" : "Contract";
      // inform the grid of the changes
      api.applyServerSideTransaction({
        route: route.slice(0, route.length - 1),
        update: [sanitizeRowForGrid(target)],
      });
    },
    removeEmployee: (route: string[]) => {
      const target = getDataAtRoute(route);
      const parent = getDataAtRoute(route.slice(0, route.length - 1));
      parent.underlings = parent.underlings.filter(
        (child: any) => child.employeeName !== target.employeeName,
      );
      if (parent.underlings.length === 0) {
        // update the parent row via transaction, as it's no longer a group
        api.applyServerSideTransaction({
          route: route.slice(0, route.length - 2),
          update: [sanitizeRowForGrid(parent)],
        });
      } else {
        // inform the grid of the changes
        api.applyServerSideTransaction({
          route: route.slice(0, route.length - 1),
          remove: [sanitizeRowForGrid(target)],
        });
      }
    },
    moveEmployee: (route: string[], to: string[]) => {
      const target = getDataAtRoute(route);
      // remove employee from old group
      fakeServer.removeEmployee(route);
      // add employee to new group
      fakeServer.addChildRow(to, target);
    },
  };
  return fakeServer;
}

function createServerSideDatasource(fakeServer: any) {
  const dataSource: IServerSideDatasource = {
    getRows: (params: IServerSideGetRowsParams) => {
      console.log("ServerSideDatasource.getRows: params = ", params);
      const request = params.request;
      const allRows = fakeServer.getData(request);
      const doingInfinite = request.startRow != null && request.endRow != null;
      const result = doingInfinite
        ? {
            rowData: allRows.slice(request.startRow, request.endRow),
            rowCount: allRows.length,
          }
        : { rowData: allRows };
      console.log("getRows: result = ", result);
      setTimeout(() => {
        params.success(result);
      }, 500);
    },
  };
  return dataSource;
}

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

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "employeeId", hide: true },
    { field: "employeeName", hide: true },
    { field: "employmentType" },
    { field: "startDate" },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      width: 235,
      flex: 1,
      sortable: false,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      field: "employeeName",
    };
  }, []);
  const rowSelection = useMemo<
    RowSelectionOptions | "single" | "multiple"
  >(() => {
    return { mode: "singleRow" };
  }, []);
  const isServerSideGroupOpenByDefault = useCallback(
    (params: IsServerSideGroupOpenByDefaultParams) => {
      const isKathrynPowers =
        params.rowNode.level == 0 &&
        params.data.employeeName == "Kathryn Powers";
      const isMabelWard =
        params.rowNode.level == 1 && params.data.employeeName == "Mabel Ward";
      return isKathrynPowers || isMabelWard;
    },
    [],
  );
  const getRowId = useCallback(
    (row: GetRowIdParams) => String(row.data.employeeId),
    [],
  );
  const isServerSideGroup = useCallback((dataItem: any) => dataItem.group, []);
  const getServerSideGroupKey = useCallback(
    (dataItem: any) => dataItem.employeeName,
    [],
  );

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/tree-data.json")
      .then((resp) => resp.json())
      .then((data: any[]) => {
        const adjustedData = [
          {
            employeeId: -1,
            employeeName: "Robert Peterson",
            employmentType: "Founder",
            startDate: "24/01/1990",
          },
          ...data,
        ];
        const fakeServer = createFakeServer(adjustedData, params.api!);
        const datasource = createServerSideDatasource(fakeServer);
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  const addToSelected = useCallback(() => {
    const selected = gridRef.current!.api.getSelectedNodes()[0];
    if (!selected) {
      console.log("No row was selected.");
      return;
    }
    const route = getRouteToNode(selected);
    const newRow = {
      employeeId: ++latestId,
      employeeName: "Bertrand Parker " + latestId,
      employmentType: "Permanent",
      startDate: "20/01/1999",
    };
    fakeServer.addChildRow(route, newRow);
  }, [latestId, fakeServer]);

  const updateSelected = useCallback(() => {
    const selected = gridRef.current!.api.getSelectedNodes()[0];
    if (!selected) {
      console.log("No row was selected.");
      return;
    }
    const route = getRouteToNode(selected);
    fakeServer.toggleEmployment(route);
  }, [fakeServer]);

  const deleteSelected = useCallback(() => {
    const selected = gridRef.current!.api.getSelectedNodes()[0];
    if (!selected) {
      console.log("No row was selected.");
      return;
    }
    const route = getRouteToNode(selected);
    fakeServer.removeEmployee(route);
  }, [fakeServer]);

  const moveSelected = useCallback(() => {
    const selected = gridRef.current!.api.getSelectedNodes()[0];
    if (!selected) {
      console.log("No row was selected.");
      return;
    }
    const route = getRouteToNode(selected);
    fakeServer.moveEmployee(route, ["Robert Peterson"]);
  }, [fakeServer]);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div className="example-wrapper">
          <div style={{ marginBottom: "5px" }}>
            <button onClick={addToSelected}>Add Child to Selected</button>
            <button onClick={updateSelected}>Update Selected</button>
            <button onClick={deleteSelected}>Delete Selected</button>
            <button onClick={moveSelected}>
              Move Selected to Robert Peterson
            </button>
          </div>

          <div style={gridStyle}>
            <AgGridReact
              ref={gridRef}
              columnDefs={columnDefs}
              defaultColDef={defaultColDef}
              autoGroupColumnDef={autoGroupColumnDef}
              rowModelType={"serverSide"}
              treeData={true}
              cacheBlockSize={10}
              rowSelection={rowSelection}
              isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
              getRowId={getRowId}
              isServerSideGroup={isServerSideGroup}
              getServerSideGroupKey={getServerSideGroupKey}
              onGridReady={onGridReady}
            />
          </div>
        </div>
      </div>
    </AgGridProvider>
  );
};

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

[Live example: Transactions with Tree Data](https://www.ag-grid.com/examples/server-side-model-tree-data/transactions-tree-data/reactFunctionalTs)

## Selection with Tree Data

Tree Data can have row selection applied in the same way as row groups. This is explained in the [SSRM Row Selection](https://www.ag-grid.com/react-data-grid/server-side-model-selection/) section.

The example below demonstrates row selection with Tree Data where the grid has been configured with `rowSelection.groupSelects = 'descendants'`. This means that selecting a row with descendants selects all of its descendants. Selecting some, but not all, of the descendants of a row places that row in the indeterminate state.

#### Selection with Tree Data

```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,
  GetServerSideGroupKey,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  IServerSideGetRowsRequest,
  IsServerSideGroup,
  IsServerSideGroupOpenByDefault,
  ModuleRegistry,
  RowModelType,
  RowSelectionOptions,
  TextFilterModule,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";

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

const modules = [
  ServerSideRowModelApiModule,
  TextFilterModule,
  ColumnsToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  TreeDataModule,
  ServerSideRowModelModule,
];

interface FakeServer {
  getData: (request: IServerSideGetRowsRequest) => any;
  addChildRow: (route: string[], newRow: any) => void;
  toggleEmployment: (route: string[]) => void;
  removeEmployee: (route: string[]) => void;
  moveEmployee: (from: string[], to: string[]) => void;
}

let fakeServer: FakeServer;

function createFakeServer(fakeServerData: any[], api: GridApi) {
  const getDataAtRoute = (route: string[]) => {
    let mutableRoute = [...route];
    let target: any = { underlings: fakeServerData };
    while (mutableRoute.length) {
      const nextRoute = mutableRoute[0];
      mutableRoute = mutableRoute.slice(1);
      target = target.underlings.find((e: any) => e.employeeName === nextRoute);
    }
    return target;
  };
  const sanitizeRowForGrid = (d: any) => {
    return {
      group: !!d.underlings && !!d.underlings.length,
      employeeId: d.employeeId,
      employeeName: d.employeeName,
      employmentType: d.employmentType,
      startDate: d.startDate,
    };
  };
  fakeServer = {
    getData: (request: IServerSideGetRowsRequest): any => {
      const extractRowsFromData: (groupKeys: string[], data: any[]) => any = (
        groupKeys: string[],
        data: any[],
      ) => {
        if (groupKeys.length === 0) {
          return data.map(sanitizeRowForGrid);
        }
        const key = groupKeys[0];
        for (let i = 0; i < data.length; i++) {
          if (data[i].employeeName === key) {
            return extractRowsFromData(
              groupKeys.slice(1),
              data[i].underlings.slice(),
            );
          }
        }
      };
      return extractRowsFromData(request.groupKeys, fakeServerData);
    },
    addChildRow: (route: string[], newRow: any) => {
      const target = getDataAtRoute(route);
      if (!target.underlings || target.underlings.length === 0) {
        target.underlings = [newRow];
        // update the parent row via transaction
        api.applyServerSideTransaction({
          route: route.slice(0, route.length - 1),
          update: [sanitizeRowForGrid(target)],
        });
      } else {
        target.underlings.push(newRow);
        // add the child row via transaction
        api.applyServerSideTransaction({
          route,
          add: [sanitizeRowForGrid(newRow)],
        });
      }
    },
    toggleEmployment: (route: string[]) => {
      const target = getDataAtRoute(route);
      // update the data at the source
      target.employmentType =
        target.employmentType === "Contract" ? "Permanent" : "Contract";
      // inform the grid of the changes
      api.applyServerSideTransaction({
        route: route.slice(0, route.length - 1),
        update: [sanitizeRowForGrid(target)],
      });
    },
    removeEmployee: (route: string[]) => {
      const target = getDataAtRoute(route);
      const parent = getDataAtRoute(route.slice(0, route.length - 1));
      parent.underlings = parent.underlings.filter(
        (child: any) => child.employeeName !== target.employeeName,
      );
      if (parent.underlings.length === 0) {
        // update the parent row via transaction, as it's no longer a group
        api.applyServerSideTransaction({
          route: route.slice(0, route.length - 2),
          update: [sanitizeRowForGrid(parent)],
        });
      } else {
        // inform the grid of the changes
        api.applyServerSideTransaction({
          route: route.slice(0, route.length - 1),
          remove: [sanitizeRowForGrid(target)],
        });
      }
    },
    moveEmployee: (route: string[], to: string[]) => {
      const target = getDataAtRoute(route);
      // remove employee from old group
      fakeServer.removeEmployee(route);
      // add employee to new group
      fakeServer.addChildRow(to, target);
    },
  };
  return fakeServer;
}

function createServerSideDatasource(fakeServer: FakeServer) {
  const dataSource: IServerSideDatasource = {
    getRows: (params) => {
      console.log("ServerSideDatasource.getRows: params = ", params);
      const request = params.request;
      const allRows = fakeServer.getData(request);
      const doingInfinite = request.startRow != null && request.endRow != null;
      const result = doingInfinite
        ? {
            rowData: allRows.slice(request.startRow, request.endRow),
            rowCount: allRows.length,
          }
        : { rowData: allRows };
      console.log("getRows: result = ", result);
      setTimeout(() => {
        params.success(result);
      }, 500);
    },
  };
  return dataSource;
}

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

  const defaultColDef = useMemo<ColDef>(() => {
    return {
      width: 235,
      flex: 1,
      sortable: false,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      field: "employeeName",
    };
  }, []);
  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "employeeId", hide: true },
    { field: "employeeName", hide: true },
    { field: "employmentType" },
    { field: "startDate" },
  ]);
  const rowSelection = useMemo<
    RowSelectionOptions | "single" | "multiple"
  >(() => {
    return {
      mode: "multiRow",
      groupSelects: "descendants",
    };
  }, []);
  const isServerSideGroupOpenByDefault = useCallback((params) => {
    const isKathrynPowers =
      params.rowNode.level == 0 && params.data.employeeName == "Kathryn Powers";
    const isMabelWard =
      params.rowNode.level == 1 && params.data.employeeName == "Mabel Ward";
    return isKathrynPowers || isMabelWard;
  }, []);
  const getRowId = useCallback((row) => String(row.data.employeeId), []);
  const isServerSideGroup = useCallback((dataItem) => dataItem.group, []);
  const getServerSideGroupKey = useCallback(
    (dataItem) => dataItem.employeeName,
    [],
  );

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/tree-data.json")
      .then((resp) => resp.json())
      .then((data: any[]) => {
        const adjustedData = [
          {
            employeeId: -1,
            employeeName: "Robert Peterson",
            employmentType: "Founder",
            startDate: "24/01/1990",
          },
          ...data,
        ];
        const fakeServer = createFakeServer(adjustedData, params.api!);
        const datasource = createServerSideDatasource(fakeServer);
        params.api!.setGridOption("serverSideDatasource", datasource);
      });
  }, []);

  return (
    <AgGridProvider modules={modules}>
      <div style={containerStyle}>
        <div className="example-wrapper">
          <div style={gridStyle}>
            <AgGridReact
              defaultColDef={defaultColDef}
              autoGroupColumnDef={autoGroupColumnDef}
              rowModelType={"serverSide"}
              treeData={true}
              columnDefs={columnDefs}
              cacheBlockSize={10}
              rowSelection={rowSelection}
              isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
              getRowId={getRowId}
              isServerSideGroup={isServerSideGroup}
              getServerSideGroupKey={getServerSideGroupKey}
              onGridReady={onGridReady}
            />
          </div>
        </div>
      </div>
    </AgGridProvider>
  );
};

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

[Live example: Selection with Tree Data](https://www.ag-grid.com/examples/server-side-model-tree-data/selecting-tree-data/reactFunctionalTs)

## Filtering Tree Data

Server-Side Tree Data Filtering should behave the same as Client-Side [Tree Data Filtering](https://www.ag-grid.com/react-data-grid/tree-data-filtering/). A group will be included if:

1. it has any children that pass the filter, or
2. it has a parent that passes the filter, or
3. its own data passes the filter

The following example demonstrates Server-Side Tree Data Filtering using the [Set Filter Tree List](https://www.ag-grid.com/react-data-grid/filter-set-tree-list/), which replicates the Tree Data structure in the filter.

- The **Group** column has the Set Filter Tree List enabled via `filterParams.treeList = true`. A Key Creator is specified to convert the path into a string.
- The **Group** column has the filter values supplied asynchronously as a nested array of strings that matches the data paths. The supplied values are at the child level only.
- The **Date** column has the Set Filter Tree List enabled via `filterParams.treeList = true`, and is grouped by year -> month -> day.
- The **Date** column has the filter values supplied asynchronously as an array of `Date` objects.
- The **Date** column has a `filterParams.keyCreator` provided to convert the `Date` values into the (string) format the server is expecting in the Filter Model.
- The **Group** and **Date** columns both have `filterParams.excelMode = 'windows'`, which allows changes to the tree filter to be applied in batches.

#### Filtering Tree Data

```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,
  GetServerSideGroupKey,
  GridApi,
  GridOptions,
  GridReadyEvent,
  IServerSideDatasource,
  ISetFilterParams,
  IsServerSideGroup,
  IsServerSideGroupOpenByDefault,
  IsServerSideGroupOpenByDefaultParams,
  KeyCreatorParams,
  ModuleRegistry,
  RowModelType,
  SetFilterValuesFuncParams,
  TextFilterModule,
  ValueFormatterParams,
  ValueGetterParams,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelModule,
  SetFilterModule,
  TreeDataModule,
} from "ag-grid-enterprise";
import { FakeServer } from "./fakeServer";

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

const modules = [
  ColumnsToolPanelModule,
  ColumnMenuModule,
  ContextMenuModule,
  TreeDataModule,
  ServerSideRowModelModule,
  SetFilterModule,
  TextFilterModule,
];

function valueGetter(params: ValueGetterParams) {
  // server is returning a string, so need to convert to `Date`.
  // could instead do this inside `IServerSideDatasource.getRows`
  return params.data.startDate ? new Date(params.data.startDate) : null;
}

function cellValueFormatter(params: ValueFormatterParams) {
  return params.value ? params.value.toLocaleDateString() : null;
}

function floatingFilterValueFormatter(params: ValueFormatterParams) {
  return params.value ? params.value.toLocaleDateString() : "(Blanks)";
}

function dateKeyCreator(params: KeyCreatorParams) {
  // this is what is being sent in the Filter Model to the server, so want the matching format
  return params.value ? params.value.toISOString() : null;
}

function treeDataKeyCreator(params: KeyCreatorParams) {
  // tree data group filter value is a string[], so convert to a unique string
  return params.value ? params.value.join(",") : null;
}

let fakeServer: any;

const getServerSideDatasource: (server: any) => IServerSideDatasource = (
  server: any,
) => {
  return {
    getRows: (params) => {
      console.log("[Datasource] - rows requested by grid: ", params.request);
      // simulating real server call with a 500ms delay
      setTimeout(() => {
        // get data for request from our fake server
        const response = server.getData(params.request);
        if (response.success) {
          // supply rows for requested block to grid
          params.success({
            rowData: response.rows,
            rowCount: response.lastRow,
          });
        } else {
          params.fail();
        }
      }, 500);
    },
  };
};

function getDatesAsync(params: SetFilterValuesFuncParams<any, Date>) {
  if (!fakeServer) {
    // wait for init
    setTimeout(() => getDatesAsync(params), 500);
    return;
  }
  let dates = fakeServer.getDates();
  if (dates) {
    // values need to match the cell value (what the `valueGetter` returns)
    dates = dates.map((isoDateString: string) =>
      isoDateString ? new Date(isoDateString) : isoDateString,
    );
  }
  // simulating real server call with a 500ms delay
  setTimeout(() => {
    params.success(dates);
  }, 500);
}

function getEmployeesAsync(params: SetFilterValuesFuncParams<any, string[]>) {
  if (!fakeServer) {
    // wait for init
    setTimeout(() => getEmployeesAsync(params), 500);
    return;
  }
  const employees = fakeServer.getEmployees();
  // simulating real server call with a 500ms delay
  setTimeout(() => {
    params.success(employees);
  }, 500);
}

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

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "employeeId", hide: true },
    { field: "employeeName", hide: true },
    { field: "employmentType" },
    {
      field: "startDate",
      valueGetter: valueGetter,
      valueFormatter: cellValueFormatter,
      filter: "agSetColumnFilter",
      filterParams: {
        treeList: true,
        excelMode: "windows",
        keyCreator: dateKeyCreator,
        valueFormatter: floatingFilterValueFormatter,
        values: getDatesAsync,
      } as ISetFilterParams<any, Date>,
    },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      width: 240,
      filter: "agTextColumnFilter",
      floatingFilter: true,
      flex: 1,
      sortable: false,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      field: "employeeName",
      filter: "agSetColumnFilter",
      filterParams: {
        treeList: true,
        excelMode: "windows",
        keyCreator: treeDataKeyCreator,
        values: getEmployeesAsync,
      } as ISetFilterParams<any, string[]>,
    };
  }, []);
  const isServerSideGroupOpenByDefault = useCallback(
    (params: IsServerSideGroupOpenByDefaultParams) => {
      // open first level by default
      return params.rowNode.level === 0;
    },
    [],
  );
  const isServerSideGroup = useCallback((dataItem: any) => {
    // indicate if node is a group
    return dataItem.underlings;
  }, []);
  const getServerSideGroupKey = useCallback((dataItem: any) => {
    // specify which group key to use
    return dataItem.employeeName;
  }, []);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/tree-data.json")
      .then((resp) => resp.json())
      .then((data: any[]) => {
        // setup the fake server with entire dataset
        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
            columnDefs={columnDefs}
            defaultColDef={defaultColDef}
            autoGroupColumnDef={autoGroupColumnDef}
            rowModelType={"serverSide"}
            treeData={true}
            isServerSideGroupOpenByDefault={isServerSideGroupOpenByDefault}
            isServerSideGroup={isServerSideGroup}
            getServerSideGroupKey={getServerSideGroupKey}
            onGridReady={onGridReady}
          />
        </div>
      </div>
    </AgGridProvider>
  );
};

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

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

## Providing Hierarchical Data via API

Below is an example of utilising `api.applyServerSideRowData` to populate data. The datasource is only used to provide root level rows, and when these are loaded the grid is provided the child row data via the API.

#### Hierarchical Data

```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,
  GetServerSideGroupKey,
  GridApi,
  GridOptions,
  GridReadyEvent,
  ICellRendererParams,
  IServerSideDatasource,
  IServerSideGetRowsParams,
  IsServerSideGroup,
  IsServerSideGroupOpenByDefault,
  IsServerSideGroupOpenByDefaultParams,
  ModuleRegistry,
  RowModelType,
  enableDevValidations,
} from "ag-grid-community";
import {
  ColumnMenuModule,
  ColumnsToolPanelModule,
  ContextMenuModule,
  ServerSideRowModelApiModule,
  ServerSideRowModelModule,
  TreeDataModule,
} from "ag-grid-enterprise";

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

const modules = [
  ColumnsToolPanelModule,
  TreeDataModule,
  ColumnMenuModule,
  ContextMenuModule,
  ServerSideRowModelModule,
  ServerSideRowModelApiModule,
];

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

  const [columnDefs, setColumnDefs] = useState<ColDef[]>([
    { field: "employeeId", hide: true },
    { field: "employeeName", hide: true },
    { field: "jobTitle" },
    { field: "employmentType" },
  ]);
  const defaultColDef = useMemo<ColDef>(() => {
    return {
      width: 240,
      flex: 1,
      sortable: false,
    };
  }, []);
  const autoGroupColumnDef = useMemo<AutoGroupColumnDef>(() => {
    return {
      field: "employeeName",
      cellRendererParams: {
        innerRenderer: (params: ICellRendererParams) => {
          // display employeeName rather than group key (employeeId)
          return params.data.employeeName;
        },
      },
    };
  }, []);
  const isServerSideGroupOpenByDefault = useCallback(
    (params: IsServerSideGroupOpenByDefaultParams) => {
      // open first two levels by default
      return params.rowNode.level < 2;
    },
    [],
  );
  const isServerSideGroup = useCallback((dataItem: any) => {
    // indicate if node is a group
    return !!dataItem.underlings;
  }, []);
  const getServerSideGroupKey = useCallback((dataItem: any) => {
    // specify which group key to use
    return dataItem.employeeId;
  }, []);

  const onGridReady = useCallback((params: GridReadyEvent) => {
    fetch("https://www.ag-grid.com/example-assets/tree-data.json")
      .then((resp) => resp.json())
      .then((data: any[]) => {
        const datasource = createServerSideDatasource(data);
        params.api!.setGridOption("serverSideDatasource", datasource);
        function createServerSideDatasource(data: any) {
          const dataSource: IServerSideDatasource = {
            getRows: (params: IServerSideGetRowsParams) => {
              console.log("ServerSideDatasource.getRows: params = ", params);
              const request = params.request;
              if (request.groupKeys.length) {
                // this example doesn't need to support lower levels.
                params.fail();
                return;
              }
              const result = {
                rowData: data.slice(request.startRow, request.endRow),
              };
              console.log("getRows: result = ", result);
              setTimeout(() => {
                params.success(result);
                const recursivelyPopulateHierarchy = (
                  route: string[],
                  node: any,
                ) => {
                  if (node.underlings) {
                    params.api!.applyServerSideRowData({
                      route,
                      successParams: {
                        rowData: node.underlings,
                        rowCount: node.underlings.length,
                      },
                    });
                    node.underlings.forEach((child: any) => {
                      recursivelyPopulateHierarchy(
                        [...route, child.employeeId],
                        child,
                      );
                    });
                  }
                };
                result.rowData.forEach((topLevelNode: any) => {
                  recursivelyPopulateHierarchy(
                    [topLevelNode.employeeId],
                    topLevelNode,
                  );
                });
              }, 200);
            },
          };
          return dataSource;
        }
      });
  }, []);

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

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

[Live example: Hierarchical Data](https://www.ag-grid.com/examples/server-side-model-tree-data/supplying-hierarchy/reactFunctionalTs)

Continue to the next section to learn how to combine [Master Detail](https://www.ag-grid.com/react-data-grid/server-side-model-master-detail/) with the SSRM.
