Building a file explorer TreeGrid in React

By radu·

View as Markdown
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 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 />.
<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.
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.
const columns = {
  name: {
    field: 'name',
    renderTreeIcon: true,
  },
};
With this setup, you're already good to go and have an interactive tree grid.
#
Basic TreeGrid example
This example is reused from the TreeGrid docs. Expand and collapse folders to inspect the nested file-system data.
open
loading demo…

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.
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 for an initial uncontrolled value:
const defaultTreeExpandState = {
  defaultExpanded: true,
  collapsedPaths: [
    ['1', '10'],
    ['3', '31'],
  ],
  expandedPaths: [['3']],
};

<TreeDataSource primaryKey="id" data={dataSource} defaultTreeExpandState={defaultTreeExpandState} />;
For fully controlled state, use treeExpandState together with onTreeExpandStateChange. The same docs page also shows the imperative Tree API methods such as expandAll and collapseAll.
#
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.
open
loading demo…

Add tree-aware selection#

Selection works at the tree level too. Configure defaultTreeSelection or treeSelection on <TreeDataSource />, and render a checkbox in the tree column with columns.renderSelectionCheckBox.
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.
#
Tree selection with checkboxes
Select and deselect branches, then use the buttons above the grid to call the Tree API for all nodes.
open
loading demo…

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 show two useful levels of customization:
  • set --infinite-expand-collapse-icon-color to recolor the default icon
  • provide a function to 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.
Rendering a custom tree icon
const renderTreeIcon: InfiniteTableColumn<FileSystemNode>['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, then continue with expand/collapse state, tree selection, and custom tree icons depending on the interaction your app needs.