# Building a file explorer TreeGrid in React

> Use Infinite Table TreeGrid and TreeDataSource to render nested data with expand state, selection, and custom tree icons.

Published: 2026-07-13  
Author: radu  
Tags: tree-grid, tree-data, react-datagrid

Canonical page: https://infinite-table.com/blog/2026/07/13/building-a-file-explorer-treegrid-in-react

An important use-case for Infinite Table is handling tree data. There's a log of scenarios where you need handling hierarchical data: product catalogs with categories, file managers, CRMs, permissions screens often need to show resources nested under resources.

Infinite Table's [TreeGrid docs](https://infinite-table.com/docs/learn/tree-grid/overview.md) cover this shape of data with two dedicated components: `<TreeDataSource />` and `<TreeGrid />`. They give tree data its own types and state model while keeping the rest of the DataGrid the same - same columns, styling, sizing, and virtualization patterns you already use in the regular DataGrid.

## Start with nested data

For tree data, use `<TreeDataSource />` instead of `<DataSource />`, and `<TreeGrid />` instead of `<InfiniteTable />`.

```tsx {1,2}
<TreeDataSource nodesKey="children" primaryKey="id" data={dataSource}>
  <TreeGrid columns={columns} />
</TreeDataSource>
```

The `nodesKey` prop tells the data source where child nodes live on each data item. In the docs, the examples use a file-system shape where folders contain a `children` array and files are leaf nodes.

```tsx {6,12}
const dataSource = [
  {
    id: '1',
    name: 'Documents',
    type: 'folder',
    children: [
      {
        id: '10',
        name: 'Report.docx',
        type: 'file',
      },
    ],
  },
];
```

Once the data has a nested structure, choose the column that should render the expand/collapse affordance by setting [`columns.renderTreeIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderTreeIcon).

```tsx {4}
const columns = {
  name: {
    field: 'name',
    renderTreeIcon: true,
  },
};
```

With this setup, you're already good to go and have an interactive tree grid.

**Example: Basic TreeGrid example**

This example is reused from the TreeGrid docs. Expand and collapse folders to inspect the nested file-system data.

```tsx
import {
  InfiniteTableColumn,
  TreeDataSource,
  TreeGrid,
} from '@infinite-table/infinite-react';

type FileSystemNode = {
  id: string;
  name: string;
  type: 'folder' | 'file';
  extension?: string;
  mimeType?: string;
  sizeInKB: number;
  children?: FileSystemNode[];
};

const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = {
  name: { field: 'name', renderTreeIcon: true, header: 'Name' },
  type: { field: 'type', header: 'Type' },
  extension: { field: 'extension', header: 'Extension' },
  mimeType: { field: 'mimeType', header: 'Mime Type' },
  size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' },
};

export default function App() {
  return (
    <TreeDataSource nodesKey="children" primaryKey="id" data={dataSource}>
      <TreeGrid columns={columns} />
    </TreeDataSource>
  );
}

const dataSource = () => {
  const nodes: FileSystemNode[] = [
    {
      id: '1',
      name: 'Documents',
      sizeInKB: 1200,
      type: 'folder',
      children: [
        {
          id: '10',
          name: 'Private',
          sizeInKB: 100,
          type: 'folder',
          children: [
            {
              id: '100',
              name: 'Report.docx',
              sizeInKB: 210,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '101',
              name: 'Vacation.docx',
              sizeInKB: 120,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '102',
              name: 'CV.pdf',
              sizeInKB: 108,
              type: 'file',
              extension: 'pdf',
              mimeType: 'application/pdf',
            },
          ],
        },
      ],
    },
    {
      id: '2',
      name: 'Desktop',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '20',
          name: 'unknown.txt',
          sizeInKB: 100,
          type: 'file',
          extension: 'txt',
          mimeType: 'text/plain',
        },
      ],
    },
    {
      id: '3',
      name: 'Media',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '30',
          name: 'Music - empty',
          sizeInKB: 0,
          type: 'folder',
          children: [],
        },
        {
          id: '31',
          name: 'Videos',
          sizeInKB: 5400,
          type: 'folder',
          children: [
            {
              id: '310',
              name: 'Vacation.mp4',
              sizeInKB: 108,
              type: 'file',
              extension: 'mp4',
              mimeType: 'video/mp4',
            },
          ],
        },
      ],
    },
  ];
  return Promise.resolve(nodes);
};
```

## Treat nodes by path, not only by id

The TreeGrid docs use the term `node` for a rendered item/row in the tree, and `node path` for the route from the root node to the current node.

The top-level of your `TreeDataSource` doesn't need to be only one item - you can have multiple roots that are siblings to each other.

```tsx title="Node path example"
const data = [
  {
    id: '1', // path: ['1']
    name: 'Documents',
    children: [
      {
        id: '10', // path: ['1', '10']
        name: 'Private',
        children: [
          {
            id: '100', // path: ['1', '10', '100']
            name: 'Report.docx',
          },
        ],
      },
    ],
  },
  {
    id: '2', // path: ['2']
    name: 'Media',
    children: [
      {
        id: '20', // path: ['2','20']
        name: 'FamilyTrip.mpeg'
      }
    ]
  }
];
```

Paths are important because tree UIs often need to preserve state at a specific location in the hierarchy. The same node id could be meaningful in different branches, while a node path describes the exact branch the user interacted with.

## Restore expand and collapse state

By default, the TreeGrid renders all nodes expanded. For product UIs, you will often want a more intentional starting point: expand the current project, collapse archived folders, or restore the state a user saved in a previous session.

Use [`defaultTreeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultTreeExpandState) for an initial uncontrolled value:

```tsx
const defaultTreeExpandState = {
  defaultExpanded: true,
  collapsedPaths: [
    ['1', '10'],
    ['3', '31'],
  ],
  expandedPaths: [['3']],
};

<TreeDataSource defaultTreeExpandState={defaultTreeExpandState} />;
```

For fully controlled state, use [`treeExpandState`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeExpandState) together with [`onTreeExpandStateChange`](https://infinite-table.com/docs/reference/datasource-props/index.md#onTreeExpandStateChange). The same docs page also shows the imperative Tree API methods such as [`expandAll`](https://infinite-table.com/docs/reference/tree-api/index.md#expandAll) and [`collapseAll`](https://infinite-table.com/docs/reference/tree-api/index.md#collapseAll).

**Example: Tree expand and collapse state**

This demo starts with a custom expand state and exposes buttons that call the Tree API to expand or collapse all nodes.

```tsx
import {
  DataSourceApi,
  InfiniteTableColumn,
  TreeDataSource,
  TreeExpandStateValue,
  TreeGrid,
} from '@infinite-table/infinite-react';
import { useState } from 'react';

type FileSystemNode = {
  id: string;
  name: string;
  type: 'folder' | 'file';
  extension?: string;
  mimeType?: string;
  sizeInKB: number;
  children?: FileSystemNode[];
};

const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = {
  name: { field: 'name', header: 'Name', renderTreeIcon: true },
  type: { field: 'type', header: 'Type' },
  extension: { field: 'extension', header: 'Extension' },
  mimeType: { field: 'mimeType', header: 'Mime Type' },
  size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' },
};

const defaultTreeExpandState: TreeExpandStateValue = {
  defaultExpanded: true,
  collapsedPaths: [
    ['1', '10'],
    ['3', '31'],
  ],
  expandedPaths: [['3']],
};

export default function App() {
  const [dataSourceApi, setDataSourceApi] =
    useState<DataSourceApi<FileSystemNode> | null>();

  return (
    <>
      <TreeDataSource
        onReady={setDataSourceApi}
        nodesKey="children"
        primaryKey="id"
        data={dataSource}
        defaultTreeExpandState={defaultTreeExpandState}
      >
        <div
          style={{
            color: 'var(--infinite-cell-color)',
            padding: '10px',
          }}
        >
          <button
            onClick={() => {
              dataSourceApi!.treeApi.expandAll();
            }}
          >
            Expand all
          </button>
          <button
            onClick={() => {
              dataSourceApi!.treeApi.collapseAll();
            }}
          >
            Collapse all
          </button>
        </div>

        <TreeGrid columns={columns} />
      </TreeDataSource>
    </>
  );
}

const dataSource = () => {
  const nodes: FileSystemNode[] = [
    {
      id: '1',
      name: 'Documents',
      sizeInKB: 1200,
      type: 'folder',
      children: [
        {
          id: '10',
          name: 'Private',
          sizeInKB: 100,
          type: 'folder',
          children: [
            {
              id: '100',
              name: 'Report.docx',
              sizeInKB: 210,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '101',
              name: 'Vacation.docx',
              sizeInKB: 120,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '102',
              name: 'CV.pdf',
              sizeInKB: 108,
              type: 'file',
              extension: 'pdf',
              mimeType: 'application/pdf',
            },
          ],
        },
      ],
    },
    {
      id: '2',
      name: 'Desktop',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '20',
          name: 'unknown.txt',
          sizeInKB: 100,
          type: 'file',
        },
      ],
    },
    {
      id: '3',
      name: 'Media',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '30',
          name: 'Music - empty',
          sizeInKB: 0,
          type: 'folder',
          children: [],
        },
        {
          id: '31',
          name: 'Videos',
          sizeInKB: 5400,
          type: 'folder',
          children: [
            {
              id: '310',
              name: 'Vacation.mp4',
              sizeInKB: 108,
              type: 'file',
              extension: 'mp4',
              mimeType: 'video/mp4',
            },
          ],
        },
      ],
    },
  ];
  return Promise.resolve(nodes);
};
```

## Add tree-aware selection

Selection works at the tree level too. Configure [`defaultTreeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#defaultTreeSelection) or [`treeSelection`](https://infinite-table.com/docs/reference/datasource-props/index.md#treeSelection) on `<TreeDataSource />`, and render a checkbox in the tree column with [`columns.renderSelectionCheckBox`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderSelectionCheckBox).

```tsx {2,7}
const defaultTreeSelection = {
  defaultSelection: true,
  deselectedPaths: [
    ['1', '10'],
    ['3', '31'],
  ],
  selectedPaths: [['3']],
};

const columns = {
  name: {
    field: 'name',
    renderTreeIcon: true,
    renderSelectionCheckBox: true,
  },
};
```

This include/exclude shape scales well for large trees because you can describe "everything is selected except these branches" or "nothing is selected except these branches" without enumerating every leaf node.

**Example: Tree selection with checkboxes**

Select and deselect branches, then use the buttons above the grid to call the Tree API for all nodes.

```tsx
import {
  DataSourceApi,
  InfiniteTableColumn,
  TreeDataSource,
  TreeGrid,
  TreeSelectionValue,
} from '@infinite-table/infinite-react';
import { useState } from 'react';

type FileSystemNode = {
  id: string;
  name: string;
  type: 'folder' | 'file';
  extension?: string;
  mimeType?: string;
  sizeInKB: number;
  children?: FileSystemNode[];
};

const columns: Record<string, InfiniteTableColumn<FileSystemNode>> = {
  name: {
    field: 'name',
    header: 'Name',
    renderTreeIcon: true,
    renderSelectionCheckBox: true,
  },
  type: { field: 'type', header: 'Type' },
  extension: { field: 'extension', header: 'Extension' },
  mimeType: { field: 'mimeType', header: 'Mime Type' },
  size: { field: 'sizeInKB', type: 'number', header: 'Size (KB)' },
};

const defaultTreeSelection: TreeSelectionValue = {
  defaultSelection: true,
  deselectedPaths: [
    ['1', '10'],
    ['3', '31'],
  ],
  selectedPaths: [['3']],
};

export default function App() {
  const [dataSourceApi, setDataSourceApi] =
    useState<DataSourceApi<FileSystemNode> | null>();

  return (
    <>
      <TreeDataSource
        onReady={setDataSourceApi}
        nodesKey="children"
        primaryKey="id"
        data={dataSource}
        defaultTreeSelection={defaultTreeSelection}
      >
        <div
          style={{
            color: 'var(--infinite-cell-color)',
            padding: 10,
            display: 'flex',
            gap: 10,
          }}
        >
          <button
            onClick={() => {
              dataSourceApi!.treeApi.selectAll();
            }}
          >
            Select all
          </button>
          <button
            onClick={() => {
              dataSourceApi!.treeApi.deselectAll();
            }}
          >
            Deselect all
          </button>
        </div>

        <TreeGrid columns={columns} />
      </TreeDataSource>
    </>
  );
}

const dataSource = () => {
  const nodes: FileSystemNode[] = [
    {
      id: '1',
      name: 'Documents',
      sizeInKB: 1200,
      type: 'folder',
      children: [
        {
          id: '10',
          name: 'Private',
          sizeInKB: 100,
          type: 'folder',
          children: [
            {
              id: '100',
              name: 'Report.docx',
              sizeInKB: 210,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '101',
              name: 'Vacation.docx',
              sizeInKB: 120,
              type: 'file',
              extension: 'docx',
              mimeType: 'application/msword',
            },
            {
              id: '102',
              name: 'CV.pdf',
              sizeInKB: 108,
              type: 'file',
              extension: 'pdf',
              mimeType: 'application/pdf',
            },
          ],
        },
      ],
    },
    {
      id: '2',
      name: 'Desktop',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '20',
          name: 'unknown.txt',
          sizeInKB: 100,
          type: 'file',
        },
      ],
    },
    {
      id: '3',
      name: 'Media',
      sizeInKB: 1000,
      type: 'folder',
      children: [
        {
          id: '31',
          name: 'Videos',
          sizeInKB: 5400,
          type: 'folder',
          children: [
            {
              id: '310',
              name: 'Vacation.mp4',
              sizeInKB: 108,
              type: 'file',
              extension: 'mp4',
              mimeType: 'video/mp4',
            },
          ],
        },
      ],
    },
  ];
  return Promise.resolve(nodes);
};
```

## Customize the tree icon

The default expand/collapse icon is enough for many apps, but file explorers, permission editors, and navigation builders usually need stronger visual signals.

The [tree icon docs](https://infinite-table.com/docs/learn/tree-grid/tree-icon-rendering.md) show two useful levels of customization:

- set `--infinite-expand-collapse-icon-color` to recolor the default icon
- provide a function to [`columns.renderTreeIcon`](https://infinite-table.com/docs/reference/infinite-table-props.md#columns.renderTreeIcon) when you want custom icons for both parent and leaf nodes

When `renderTreeIcon` is a function, it receives tree-specific row information. Check `rowInfo.isParentNode` before reading parent-only state such as `rowInfo.nodeExpanded`.

```tsx title="Rendering a custom tree icon"
const renderTreeIcon = ({ rowInfo, toggleCurrentTreeNode }) => {
  if (!rowInfo.isParentNode) {
    return <FileIcon />;
  }

  return (
    <FolderIcon open={rowInfo.nodeExpanded} onClick={toggleCurrentTreeNode} />
  );
};
```

## When to reach for TreeGrid

Use TreeGrid when the hierarchy is part of the data model:

- file-system or document-library UIs
- organization charts and team member lists
- product catalogs with nested categories
- permission editors with resources and sub-resources
- project plans with parent tasks and child tasks

Use regular row grouping when the hierarchy is derived from flat data. Use TreeGrid when the hierarchy already exists in the records you load.

Start with the [TreeGrid overview](https://infinite-table.com/docs/learn/tree-grid/overview.md), then continue with [expand/collapse state](https://infinite-table.com/docs/learn/tree-grid/tree-expand-and-collapse-state.md), [tree selection](https://infinite-table.com/docs/learn/tree-grid/tree-selection.md), and [custom tree icons](https://infinite-table.com/docs/learn/tree-grid/tree-icon-rendering.md) depending on the interaction your app needs.
