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
rowGrouper
expandedGroupIds
onExpandedGroupIdsChange
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…
function rowGrouper ( rows : readonly Row [], columnKey : string ) {
return Object . groupBy ( rows , ( row ) => row [ columnKey ]);
}
Function that groups rows by column key. Returns object with group values as keys. const [ expandedGroupIds , setExpandedGroupIds ] = useState < ReadonlySet < unknown >>(
new Set ()
);
Set of currently expanded group IDs. Controls which groups show children. function handleExpandedChange ( ids : Set < unknown >) {
setExpandedGroupIds ( ids );
}
Callback when groups are expanded/collapsed.
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
Home : First cell in row
End : Last cell in row
Ctrl+Home : First cell in grid
Ctrl+End : Last cell in grid
// 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 ;
}