Skip to main content
Row grouping allows you to organize rows into a hierarchical tree structure with expandable and collapsible groups. This is implemented using the TreeDataGrid component.

Basic Usage

Use TreeDataGrid instead of DataGrid for row grouping:
import { useState } from 'react';
import { TreeDataGrid, type Column } from 'react-data-grid';

interface Row {
  id: number;
  country: string;
  city: string;
  name: string;
  sales: number;
}

const columns: Column<Row>[] = [
  { key: 'country', name: 'Country' },
  { key: 'city', name: 'City' },
  { key: 'name', name: 'Name' },
  { key: 'sales', name: 'Sales' }
];

const rows: Row[] = [
  { id: 1, country: 'USA', city: 'New York', name: 'John', sales: 1000 },
  { id: 2, country: 'USA', city: 'New York', name: 'Jane', sales: 1500 },
  { id: 3, country: 'USA', city: 'Los Angeles', name: 'Bob', sales: 2000 },
  { id: 4, country: 'Canada', city: 'Toronto', name: 'Alice', sales: 1200 },
];

function rowGrouper(rows: readonly Row[], columnKey: string) {
  return Object.groupBy(rows, (row) => row[columnKey as keyof Row]);
}

function MyTreeGrid() {
  const [expandedGroupIds, setExpandedGroupIds] = useState<ReadonlySet<unknown>>(new Set());
  
  return (
    <TreeDataGrid
      columns={columns}
      rows={rows}
      rowKeyGetter={(row) => row.id}
      groupBy={['country', 'city']}
      rowGrouper={rowGrouper}
      expandedGroupIds={expandedGroupIds}
      onExpandedGroupIdsChange={setExpandedGroupIds}
    />
  );
}
This creates a hierarchical structure:
▶ USA (3)
▼ Canada (1)
  ▶ Toronto (1)
    John - 1000
When expanded:
▼ USA (3)
  ▼ New York (2)
    John - 1000
    Jane - 1500
  ▶ Los Angeles (1)
▼ Canada (1)
  ▼ Toronto (1)
    Alice - 1200

Required Props

groupBy={['country', 'city']}
Array of column keys to group by. Order determines hierarchy:
  • First key: top-level groups
  • Second key: nested under first
  • And so on…

Row Grouper Function

The rowGrouper function determines how rows are grouped:
function rowGrouper(
  rows: readonly Row[],
  columnKey: string
): Record<string, readonly Row[]> {
  return Object.groupBy(rows, (row) => row[columnKey]);
}
Implement custom grouping logic:
function customRowGrouper(rows: readonly Row[], columnKey: string) {
  const groups: Record<string, Row[]> = {};
  
  for (const row of rows) {
    let key: string;
    
    if (columnKey === 'date') {
      // Group by month
      const date = new Date(row[columnKey]);
      key = `${date.getFullYear()}-${date.getMonth() + 1}`;
    } else if (columnKey === 'sales') {
      // Group by sales range
      const sales = row[columnKey];
      if (sales < 1000) key = 'Low';
      else if (sales < 5000) key = 'Medium';
      else key = 'High';
    } else {
      key = String(row[columnKey]);
    }
    
    if (!groups[key]) groups[key] = [];
    groups[key].push(row);
  }
  
  return groups;
}

Group ID Generation

By default, group IDs are generated by concatenating parent and group keys:
// Default implementation from src/TreeDataGrid.tsx
function defaultGroupIdGetter(groupKey: string, parentId?: string) {
  return parentId !== undefined ? `${parentId}__${groupKey}` : groupKey;
}
Customize with your own function:
function customGroupIdGetter(groupKey: string, parentId?: string) {
  return parentId ? `${parentId}-${groupKey}` : groupKey;
}

<TreeDataGrid
  // ...
  groupIdGetter={customGroupIdGetter}
/>
Group IDs must be unique across the entire tree structure.

Expand/Collapse All

Control which groups are expanded programmatically:
function MyTreeGrid() {
  const [expandedGroupIds, setExpandedGroupIds] = useState<ReadonlySet<unknown>>(new Set());
  
  // Get all possible group IDs (implement based on your data)
  const allGroupIds = useMemo(() => {
    const ids = new Set<unknown>();
    // Traverse your grouped data structure and collect all group IDs
    return ids;
  }, [rows]);
  
  function expandAll() {
    setExpandedGroupIds(new Set(allGroupIds));
  }
  
  function collapseAll() {
    setExpandedGroupIds(new Set());
  }
  
  return (
    <>
      <button onClick={expandAll}>Expand All</button>
      <button onClick={collapseAll}>Collapse All</button>
      <TreeDataGrid
        columns={columns}
        rows={rows}
        groupBy={['country', 'city']}
        rowGrouper={rowGrouper}
        expandedGroupIds={expandedGroupIds}
        onExpandedGroupIdsChange={setExpandedGroupIds}
      />
    </>
  );
}
Save and restore expanded state:
function MyTreeGrid() {
  const [expandedGroupIds, setExpandedGroupIds] = useState<ReadonlySet<unknown>>(() => {
    const saved = localStorage.getItem('expandedGroups');
    if (saved) {
      return new Set(JSON.parse(saved));
    }
    return new Set();
  });
  
  useEffect(() => {
    localStorage.setItem('expandedGroups', JSON.stringify(Array.from(expandedGroupIds)));
  }, [expandedGroupIds]);
  
  return (
    <TreeDataGrid
      columns={columns}
      rows={rows}
      groupBy={['country', 'city']}
      rowGrouper={rowGrouper}
      expandedGroupIds={expandedGroupIds}
      onExpandedGroupIdsChange={setExpandedGroupIds}
    />
  );
}

Custom Group Cell Renderer

Customize how group rows are rendered:
import type { RenderGroupCellProps } from 'react-data-grid';

function CustomGroupCell({
  groupKey,
  childRows,
  isExpanded,
  toggleGroup
}: RenderGroupCellProps<Row>) {
  const count = childRows.length;
  
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
      <button
        onClick={toggleGroup}
        aria-expanded={isExpanded}
        style={{ padding: '4px 8px', cursor: 'pointer' }}
      >
        {isExpanded ? '▼' : '▶'}
      </button>
      <strong>{groupKey}</strong>
      <span style={{ color: '#666' }}>({count} items)</span>
    </div>
  );
}

const columns: Column<Row>[] = [
  {
    key: 'country',
    name: 'Country',
    renderGroupCell: CustomGroupCell
  },
  { key: 'city', name: 'City' },
  { key: 'name', name: 'Name' },
  { key: 'sales', name: 'Sales' }
];
The renderGroupCell prop is only used for columns specified in the groupBy array.

Keyboard Navigation

TreeDataGrid implements the Treegrid ARIA pattern:
  • → (Right): Expand collapsed group
  • ← (Left): Collapse expanded group or move to parent
  • ↑ (Up): Move to previous row
  • ↓ (Down): Move to next row
// From src/TreeDataGrid.tsx - keyboard handling
if (
  idx === -1 &&
  ((event.key === leftKey && row.isExpanded) ||
   (event.key === rightKey && !row.isExpanded))
) {
  event.preventDefault();
  event.preventGridDefault();
  toggleGroup(row.id);
}

Dynamic Row Heights

Use different heights for group rows vs regular rows:
import type { RowHeightArgs } from 'react-data-grid';

function getRowHeight(args: RowHeightArgs<Row>): number {
  if (args.type === 'GROUP') {
    return 50; // Taller group rows
  }
  return 35; // Standard row height
}

<TreeDataGrid
  columns={columns}
  rows={rows}
  rowHeight={getRowHeight}
  groupBy={['country', 'city']}
  rowGrouper={rowGrouper}
  expandedGroupIds={expandedGroupIds}
  onExpandedGroupIdsChange={setExpandedGroupIds}
/>
From src/types.ts:
type RowHeightArgs<TRow> =
  | { type: 'ROW'; row: TRow }
  | { type: 'GROUP'; row: GroupRow<TRow> };
Use the type discriminator to distinguish between regular and group rows.

Row Selection in Groups

Row selection works with grouped data:
import { useState } from 'react';
import { TreeDataGrid, SelectColumn } from 'react-data-grid';

function MyTreeGrid() {
  const [selectedRows, setSelectedRows] = useState<ReadonlySet<number>>(new Set());
  const [expandedGroupIds, setExpandedGroupIds] = useState<ReadonlySet<unknown>>(new Set());
  
  const columns = [
    SelectColumn,
    { key: 'country', name: 'Country' },
    { key: 'city', name: 'City' },
    { key: 'name', name: 'Name' }
  ];
  
  return (
    <TreeDataGrid
      columns={columns}
      rows={rows}
      rowKeyGetter={(row) => row.id}
      selectedRows={selectedRows}
      onSelectedRowsChange={setSelectedRows}
      groupBy={['country', 'city']}
      rowGrouper={rowGrouper}
      expandedGroupIds={expandedGroupIds}
      onExpandedGroupIdsChange={setExpandedGroupIds}
    />
  );
}
Group Row Selection Behavior (from src/TreeDataGrid.tsx):
  • Selecting a group row selects all its children
  • A group row appears selected if all children are selected
  • Unselecting a group row unselects all children
// If all children are selected, the group row appears selected
const isGroupRowSelected = row.childRows.every((cr) =>
  rawSelectedRows.has(rawRowKeyGetter(cr))
);

Limitations

The following DataGrid features are not supported in TreeDataGrid:1. Column Groups
// ❌ Not supported
<TreeDataGrid
  columns={[
    {
      name: 'Group',
      children: [/* ... */]
    }
  ]}
/>
2. Drag Fill (onFill)
// ❌ Not supported
<TreeDataGrid
  onFill={(event) => /* ... */}
/>
3. Row Selection Disabling
// ❌ Not supported
<TreeDataGrid
  isRowSelectionDisabled={(row) => /* ... */}
/>
4. Copy/Paste on Group Rows
  • Copy/paste operations are disabled for group rows
  • Only work on leaf (non-group) rows

API Reference

TreeDataGrid Props

groupBy

groupBy: readonly string[]
Required. Array of column keys to group by. Order determines hierarchy.

rowGrouper

rowGrouper: (rows: readonly R[], columnKey: string) => Record<string, readonly R[]>
Required. Function that groups rows by column key.

expandedGroupIds

expandedGroupIds: ReadonlySet<unknown>
Required. Set of currently expanded group IDs.

onExpandedGroupIdsChange

onExpandedGroupIdsChange: (expandedGroupIds: Set<unknown>) => void
Required. Callback when groups are expanded/collapsed.

groupIdGetter

groupIdGetter?: (groupKey: string, parentId?: string) => string
Optional. Function to generate unique group IDs. Default concatenates with __.

rowHeight

rowHeight?: number | ((args: RowHeightArgs<R>) => number)
Optional. Row height in pixels or function returning height. Function receives RowHeightArgs to distinguish group vs regular rows.

GroupRow Type

interface GroupRow<TRow> {
  readonly childRows: readonly TRow[];
  readonly id: string;
  readonly parentId: unknown;
  readonly groupKey: unknown;
  readonly isExpanded: boolean;
  readonly level: number;
  readonly posInSet: number;
  readonly setSize: number;
  readonly startRowIndex: number;
}

RenderGroupCellProps

interface RenderGroupCellProps<TRow, TSummaryRow = unknown> {
  groupKey: unknown;
  column: CalculatedColumn<TRow, TSummaryRow>;
  row: GroupRow<TRow>;
  childRows: readonly TRow[];
  isExpanded: boolean;
  tabIndex: number;
  toggleGroup: () => void;
}

Build docs developers (and LLMs) love