MRT logoMaterial React Table

Legacy V2 Docs

Expanding Sub-Rows (Tree Data) Feature Guide

Material React Table has support for expanding sub-rows or tree data. This feature is useful for displaying hierarchical data. The sub-rows can be expanded and collapsed by clicking on the expand/collapse icon.

NOTE: This feature is for expanding rows of the same data type. If you want to add expansion of more data for the same row, check out the Detail Panel Feature Guide.

Relevant Table Options

1
boolean
TanStack Table Expanding Docs
2
Array<TData>
Usage Docs
3
boolean
true
MRT Expanding Sub Rows Docs
4
boolean
MRT Expanding Sub Rows Docs
5
(dataRow: TData) => TData[]
6
boolean
false
TanStack Filtering Docs
7
() => MRT_RowModel<TData>
8
(row: Row<TData>) => boolean
TanStack Table Expanding Docs
9
(row: Row<TData>) => boolean
TanStack Table Expanding Docs
10
(originalRow: TData, index: number, parent?: MRT_Row<TData>) => string
TanStack Table Core Table Docs
11
(originalRow: TData, index: number) => undefined | TData[]
TanStack Table Core Table Docs
12
false | 'reorder' | 'remove'
reorder
TanStack Table Grouping Docs
13
boolean
TanStack Table Expanding Docs
14
number
100
TanStack Table Filtering Docs
15
IconButtonProps | ({ table }) => IconButtonProps
Material UI IconButton Props
16
IconButtonProps | ({ row, table }) => IconButtonProps
Material UI IconButton Props
17
OnChangeFn<ExpandedState>
TanStack Table Expanding Docs
18
boolean
TanStack Table Expanding Docs
19
'first' | 'last'
'first'

Relevant State Options

1
Record<string, boolean> | boolean
{}
TanStack Table Expanding Docs

Enable Expanding Sub-Rows

To enable expanding sub-rows, you must first set the enableExpanding table option to true.

However, your data must also be formatted in a way to allow for expanding rows that are in some way related to each other. By default, Material React Table will look for a special subRows property on each row of your data and treat any array of rows that it finds as the sub-rows for that row. You can customize or override this behavior by passing a custom getSubRows table option.

const data = [
{
id: 1,
name: 'John Doe',
subRows: [
{
id: 2,
name: 'Jane Doe',
},
//more sub rows...
],
},
//more rows...
];
const table = useMaterialReactTable({
columns,
data,
enableExpanding: true,
getSubRows: (originalRow) => originalRow.subRows, //default, can customize
});
return <MaterialReactTable table={table} />;

Expand All Rows Button

By default, Material React Table will show the expand all button in the expand column header. You can disable this by setting the enableExpandAll table option to false.

const table = useMaterialReactTable({
columns,
data,
enableExpanding: true,
enableExpandAll: false, //hide expand all button in header
});
DylanMurray261 Erdman FordEast DaphneKentucky
RaquelKohler769 Dominic GroveColumbusOhio
1-2 of 2

Source Code

1import { useMemo } from 'react';
2import {
3 MaterialReactTable,
4 useMaterialReactTable,
5 type MRT_ColumnDef,
6} from 'material-react-table';
7
8export type Person = {
9 firstName: string;
10 lastName: string;
11 address: string;
12 city: string;
13 state: string;
14 subRows?: Person[]; //Each person can have sub rows of more people
15};
16
17export const data = [
18 {
19 firstName: 'Dylan',
20 lastName: 'Murray',
21 address: '261 Erdman Ford',
22 city: 'East Daphne',
23 state: 'Kentucky',
24 subRows: [
25 {
26 firstName: 'Ervin',
27 lastName: 'Reinger',
28 address: '566 Brakus Inlet',
29 city: 'South Linda',
30 state: 'West Virginia',
31 subRows: [
32 {
33 firstName: 'Jordane',
34 lastName: 'Homenick',
35 address: '1234 Brakus Inlet',
36 city: 'South Linda',
37 state: 'West Virginia',
38 },
39 ],
40 },
41 {
42 firstName: 'Brittany',
43 lastName: 'McCullough',
44 address: '722 Emie Stream',
45 city: 'Lincoln',
46 state: 'Nebraska',
47 },
48 ],
49 },
50 {
51 firstName: 'Raquel',
52 lastName: 'Kohler',
53 address: '769 Dominic Grove',
54 city: 'Columbus',
55 state: 'Ohio',
56 subRows: [
57 {
58 firstName: 'Branson',
59 lastName: 'Frami',
60 address: '32188 Larkin Turnpike',
61 city: 'Charleston',
62 state: 'South Carolina',
63 },
64 ],
65 },
66];
67
68const Example = () => {
69 const columns = useMemo<MRT_ColumnDef<Person>[]>(
70 //column definitions...
98 );
99
100 const table = useMaterialReactTable({
101 columns,
102 data,
103 enableExpandAll: false, //hide expand all double arrow in column header
104 enableExpanding: true,
105 });
106
107 return <MaterialReactTable table={table} />;
108};
109
110export default Example;
111

Generate Sub Rows with getSubRows

If your data is not yet in a tree structure, but the data has relationships that can be parsed into a tree, you can use the getSubRows table option to let TanStack Table find the sub rows for each row.

There are a couple key things you have to do to make this work:

  1. Only pass in root (top level) rows in your data prop.

  2. Set the getSubRows table option to a function that scans all the rest of your data and returns the sub rows for a given row.

This can sometimes be useful in combination with lazy loading sub rows.

NOTE: Be conscious of the performance implications of the getSubRows function. It will be called for every row in your table, so it should be performant.

HenryLynchCamden.Macejkovic@yahoo.comCalifornia
MckennaFriesenVeda_Feeney@yahoo.comNew York
1-2 of 2

Source Code

1import { useMemo } from 'react';
2import {
3 MaterialReactTable,
4 useMaterialReactTable,
5 type MRT_ColumnDef,
6} from 'material-react-table';
7
8export type Employee = {
9 id: string;
10 firstName: string;
11 lastName: string;
12 email: string;
13 state: string;
14 managerId: string | null;
15};
16
17//flat data that TanStack Table's getSubRows() function will parse into a tree
18export const data: Employee[] = [
19 {
20 id: '9s41rp',
21 firstName: 'Kelvin',
22 lastName: 'Langosh',
23 email: 'Jerod14@hotmail.com',
24 state: 'Ohio',
25 managerId: '08m6rx',
26 },
27 {
28 id: '08m6rx',
29 firstName: 'Molly',
30 lastName: 'Purdy',
31 email: 'Hugh.Dach79@hotmail.com',
32 state: 'Rhode Island',
33 managerId: '5ymtrc',
34 },
35 {
36 id: '5ymtrc',
37 firstName: 'Henry',
38 lastName: 'Lynch',
39 email: 'Camden.Macejkovic@yahoo.com',
40 state: 'California',
41 managerId: null, //top of a tree
42 },
43 {
44 id: 'ek5b97',
45 firstName: 'Glenda',
46 lastName: 'Douglas',
47 email: 'Eric0@yahoo.com',
48 state: 'Montana',
49 managerId: '08m6rx',
50 },
51 {
52 id: 'xxtydd',
53 firstName: 'Leone',
54 lastName: 'Williamson',
55 email: 'Ericka_Mueller52@yahoo.com',
56 state: 'Colorado',
57 managerId: '08m6rx',
58 },
59 {
60 id: 'wzxj9m',
61 firstName: 'Mckenna',
62 lastName: 'Friesen',
63 email: 'Veda_Feeney@yahoo.com',
64 state: 'New York',
65 managerId: null, //top of a tree
66 },
67 {
68 id: '21dwtz',
69 firstName: 'Wyman',
70 lastName: 'Jast',
71 email: 'Melvin.Pacocha@yahoo.com',
72 state: 'Montana',
73 managerId: 'wzxj9m',
74 },
75 {
76 id: 'o8oe4k',
77 firstName: 'Janick',
78 lastName: 'Willms',
79 email: 'Delfina12@gmail.com',
80 state: 'Nebraska',
81 managerId: 'wzxj9m',
82 },
83];
84
85const Example = () => {
86 const columns = useMemo<MRT_ColumnDef<Employee>[]>(
87 //column definitions...
109 );
110
111 //only root rows with no managerId
112 const rootData = useMemo(() => data.filter((r) => !r.managerId), [data]);
113
114 const table = useMaterialReactTable({
115 columns,
116 data: rootData,
117 enableExpanding: true,
118 //note: performance of this example should be improved with hash maps. This is currently 0(n^2)
119 getSubRows: (row) => data.filter((r) => r.managerId === row.id),
120 });
121
122 return <MaterialReactTable table={table} />;
123};
124
125export default Example;
126

Expanded Rows Pagination Behavior

By default, Material React Table will treat expanded sub-rows the same as any other row when it comes to pagination. This means that some expanded rows may be on the next page. You can change this behavior by setting the paginateExpandedRows table option to false.

const table = useMaterialReactTable({
columns,
data,
enableExpanding: true,
paginateExpandedRows: false, //expanded rows will be on the same page as their parent row
});

Expanded Leaf Row Filtering Behavior

If you are using the filtering features alongside sub-row features, then there are a few behaviors and customizations you should be aware of.

Filter From Leaf Rows

By default, filtering is done from parent rows down (so if a parent row is filtered out, all of its children will be filtered out as well). Setting the filterFromLeafRows table option to true will cause filtering to be done from leaf rows up (which means parent rows will be kept so long as one of their child, or grand-child, etc. rows pass the filtering).

const table = useMaterialReactTable({
columns,
data,
enableExpanding: true,
filterFromLeafRows: true, //search for child rows and preserve parent rows
});

Max Leaf Row Filter Depth

By default, filtering is done for all rows (max depth of 100), no matter if they are root level parent rows or the child leaf rows of a parent row. Setting the maxLeafRowFilterDepth table option to 0 will cause filtering to only be applied to the root level parent rows, with all sub-rows remaining unfiltered. Similarly, setting this option to 1 will cause filtering to only be applied to child leaf rows 1 level deep, and so on.

This is useful for situations where you want a row's entire child hierarchy to be visible, regardless of the applied filter.

const table = useMaterialReactTable({
columns,
data,
enableExpanding: true,
maxLeafRowFilterDepth: 0, //When filtering root rows, keep all child rows of the passing parent rows
});

Expand All Rows By Default

You can manage the initial state of the expanded rows with the expanded state option in either the initialState or state props.

For example, you may want all rows to be expanded by default. To do this, you can simply set the expanded state option to true.

const table = useMaterialReactTable({
columns,
data,
enableExpanding: true,
initialState: { expanded: true }, //all rows expanded by default
});
DylanMurray261 Erdman FordEast DaphneKentucky
ErvinReinger566 Brakus InletSouth LindaWest Virginia
JordaneHomenick1234 Brakus InletSouth LindaWest Virginia
JordanClarkson4882 Palm RdSan FranciscoCalifornia
BrittanyMcCullough722 Emie StreamLincolnNebraska
RaquelKohler769 Dominic GroveColumbusOhio
BransonFrami32188 Larkin TurnpikeCharlestonSouth Carolina
1-2 of 2

Source Code

1import { useMemo } from 'react';
2import {
3 MaterialReactTable,
4 useMaterialReactTable,
5 type MRT_ColumnDef,
6} from 'material-react-table';
7
8export type Person = {
9 firstName: string;
10 lastName: string;
11 address: string;
12 city: string;
13 state: string;
14 subRows?: Person[]; //Each person can have sub rows of more people
15};
16
17export const data: Person[] = [
18 {
19 firstName: 'Dylan',
20 lastName: 'Murray',
21 address: '261 Erdman Ford',
22 city: 'East Daphne',
23 state: 'Kentucky',
24 subRows: [
25 {
26 firstName: 'Ervin',
27 lastName: 'Reinger',
28 address: '566 Brakus Inlet',
29 city: 'South Linda',
30 state: 'West Virginia',
31 subRows: [
32 {
33 firstName: 'Jordane',
34 lastName: 'Homenick',
35 address: '1234 Brakus Inlet',
36 city: 'South Linda',
37 state: 'West Virginia',
38 },
39 {
40 firstName: 'Jordan',
41 lastName: 'Clarkson',
42 address: '4882 Palm Rd',
43 city: 'San Francisco',
44 state: 'California',
45 },
46 ],
47 },
48 {
49 firstName: 'Brittany',
50 lastName: 'McCullough',
51 address: '722 Emie Stream',
52 city: 'Lincoln',
53 state: 'Nebraska',
54 },
55 ],
56 },
57 {
58 firstName: 'Raquel',
59 lastName: 'Kohler',
60 address: '769 Dominic Grove',
61 city: 'Columbus',
62 state: 'Ohio',
63 subRows: [
64 {
65 firstName: 'Branson',
66 lastName: 'Frami',
67 address: '32188 Larkin Turnpike',
68 city: 'Charleston',
69 state: 'South Carolina',
70 },
71 ],
72 },
73];
74
75const Example = () => {
76 const columns = useMemo<MRT_ColumnDef<Person>[]>(
77 //column definitions...
105 );
106
107 const table = useMaterialReactTable({
108 columns,
109 data,
110 enableExpandAll: false, //hide expand all double arrow in column header
111 enableExpanding: true,
112 filterFromLeafRows: true, //apply filtering to all rows instead of just parent rows
113 getSubRows: (row) => row.subRows, //default
114 initialState: { expanded: true }, //expand all rows by default
115 paginateExpandedRows: false, //When rows are expanded, do not count sub-rows as number of rows on the page towards pagination
116 });
117
118 return <MaterialReactTable table={table} />;
119};
120
121export default Example;
122

Expand Root Rows Only By Default

Here is a slightly more complex initial expanded state example where all the root rows are expanded by default, but none of the sub rows themselves are expanded by default. We just need to find all of the root row ids and set their key in the expanded initialState option to true.

DylanMurray261 Erdman FordEast DaphneKentucky
ErvinReinger566 Brakus InletSouth LindaWest Virginia
BrittanyMcCullough722 Emie StreamLincolnNebraska
RaquelKohler769 Dominic GroveColumbusOhio
BransonFrami32188 Larkin TurnpikeCharlestonSouth Carolina
1-5 of 5

Source Code

1import { useMemo } from 'react';
2import {
3 MaterialReactTable,
4 type MRT_ExpandedState,
5 type MRT_ColumnDef,
6 useMaterialReactTable,
7} from 'material-react-table';
8import { Button } from '@mui/material';
9
10export type Person = {
11 id: string;
12 firstName: string;
13 lastName: string;
14 address: string;
15 city: string;
16 state: string;
17 subRows?: Person[]; //Each person can have sub rows of more people
18};
19
20//data definitions...
96
97const Example = () => {
98 const columns = useMemo<MRT_ColumnDef<Person>[]>(
99 //column definitions...
127 );
128
129 const initialExpandedRootRows = useMemo<MRT_ExpandedState>(
130 () =>
131 data
132 .map((originalRow) => originalRow.id) //get all the root row ids, use recursion for additional levels
133 .reduce((a, v) => ({ ...a, [v]: true }), {}), //convert to an object with all the ids as keys and `true` as values
134 [],
135 );
136
137 const table = useMaterialReactTable({
138 columns,
139 data,
140 enableExpanding: true,
141 getRowId: (originalRow) => originalRow.id,
142 initialState: { expanded: initialExpandedRootRows }, //only expand the root rows by default
143 renderTopToolbarCustomActions: ({ table }) => (
144 <Button onClick={() => table.resetExpanded()}>Reset Expanded</Button>
145 ),
146 });
147
148 return <MaterialReactTable table={table} />;
149};
150
151export default Example;
152

Customize Expand Column

You can customize the expand column by using the displayColumnDefOptions table option.

const table = useMaterialReactTable({
columns,
data,
enableGrouping: true,
displayColumnDefOptions: {
'mrt-row-expand': {
enableResizing: true, //allow resizing
size: 120, //make the expand column wider
},
},
});

Lazy Load Sub Rows

If you have a ton of nested data that you want to display, but you don't want to fetch it all up front, you can set up Material React Table to only fetch the sub-rows data when the user expands the row.

There are quite a few ways in which you could implement fetching sub-rows lazily. This example is just one way to do it.

The main concept to understand from this example is that you can manage the expanded state option in your own scope, and fetch the data for your table based on that state.

How your data is structured from the server is up to you. It is usually easiest to have the server do the hard work and return the data in a nested tree structure, but you can also return the data in a flat structure and use the getSubRows table option to parse the data into a tree structure.

0-0 of 0

Source Code

1import { useMemo, useState } from 'react';
2import {
3 MaterialReactTable,
4 useMaterialReactTable,
5 type MRT_ColumnDef,
6 type MRT_PaginationState,
7 type MRT_SortingState,
8 type MRT_ExpandedState,
9} from 'material-react-table';
10import {
11 QueryClient,
12 QueryClientProvider,
13 keepPreviousData,
14 useQuery,
15} from '@tanstack/react-query'; //note: this is TanStack React Query V5
16
17//Your API response shape will probably be different. Knowing a total row count is important though.
18type UserApiResponse = {
19 data: Array<User>;
20 meta: {
21 totalRowCount: number;
22 };
23};
24
25type User = {
26 id: string;
27 firstName: string;
28 lastName: string;
29 email: string;
30 state: string;
31 managerId: string | null; //row's parent row id
32 subordinateIds: string[]; //or some type of boolean that indicates that there are sub-rows
33};
34
35const columns: MRT_ColumnDef<User>[] = [
36 //column definitions...
54];
55
56const Example = () => {
57 const [sorting, setSorting] = useState<MRT_SortingState>([]);
58 const [pagination, setPagination] = useState<MRT_PaginationState>({
59 pageIndex: 0,
60 pageSize: 10,
61 });
62 const [expanded, setExpanded] = useState<MRT_ExpandedState>({}); //Record<string, boolean> | true
63
64 //which rows have sub-rows expanded and need their direct sub-rows to be included in the API call
65 const expandedRowIds: string[] | 'all' = useMemo(
66 () =>
67 expanded === true
68 ? 'all'
69 : Object.entries(expanded)
70 .filter(([_managerId, isExpanded]) => isExpanded)
71 .map(([managerId]) => managerId),
72 [expanded],
73 );
74
75 const {
76 data: { data = [], meta } = {},
77 isError,
78 isRefetching,
79 isLoading,
80 } = useFetchUsers({
81 pagination,
82 sorting,
83 expandedRowIds,
84 });
85
86 //get data for root rows only (top of the tree data)
87 const rootData = useMemo(() => data.filter((r) => !r.managerId), [data]);
88
89 const table = useMaterialReactTable({
90 columns,
91 data: rootData,
92 enableExpanding: true, //enable expanding column
93 enableFilters: false,
94 //tell MRT which rows have additional sub-rows that can be fetched
95 getRowCanExpand: (row) => !!row.original.subordinateIds.length, //just some type of boolean
96 //identify rows by the user's id
97 getRowId: (row) => row.id,
98 //if data is delivered in a flat array, MRT can convert it to a tree structure
99 //though it's usually better if the API can construct the nested structure before this point
100 getSubRows: (row) => data.filter((r) => r.managerId === row.id), //parse flat array into tree structure
101 // paginateExpandedRows: false, //the back-end in this example is acting as if this option is false
102 manualPagination: true, //turn off built-in client-side pagination
103 manualSorting: true, //turn off built-in client-side sorting
104 muiToolbarAlertBannerProps: isError
105 ? {
106 color: 'error',
107 children: 'Error loading data',
108 }
109 : undefined,
110 onExpandedChange: setExpanded,
111 onPaginationChange: setPagination,
112 onSortingChange: setSorting,
113 rowCount: meta?.totalRowCount ?? 0,
114 state: {
115 expanded,
116 isLoading,
117 pagination,
118 showAlertBanner: isError,
119 showProgressBars: isRefetching,
120 sorting,
121 },
122 });
123
124 return <MaterialReactTable table={table} />;
125};
126
127const queryClient = new QueryClient();
128
129const ExampleWithReactQueryProvider = () => (
130 //App.tsx or AppProviders file. Don't just wrap this component with QueryClientProvider! Wrap your whole App!
131 <QueryClientProvider client={queryClient}>
132 <Example />
133 </QueryClientProvider>
134);
135
136export default ExampleWithReactQueryProvider;
137
138//fetch user hook
139const useFetchUsers = ({
140 pagination,
141 sorting,
142 expandedRowIds,
143}: {
144 pagination: MRT_PaginationState;
145 sorting: MRT_SortingState;
146 expandedRowIds: string[] | 'all';
147}) => {
148 return useQuery<UserApiResponse>({
149 queryKey: [
150 'users', //give a unique key for this query
151 pagination.pageIndex, //refetch when pagination.pageIndex changes
152 pagination.pageSize, //refetch when pagination.pageSize changes
153 sorting, //refetch when sorting changes
154 expandedRowIds,
155 ],
156 queryFn: async () => {
157 const fetchURL = new URL(
158 '/api/treedata',
159 process.env.NODE_ENV === 'production'
160 ? 'https://www.material-react-table.com'
161 : 'http://localhost:3000',
162 );
163
164 //read our state and pass it to the API as query params
165 fetchURL.searchParams.set(
166 'start',
167 `${pagination.pageIndex * pagination.pageSize}`,
168 );
169 fetchURL.searchParams.set('size', `${pagination.pageSize}`);
170 fetchURL.searchParams.set('sorting', JSON.stringify(sorting ?? []));
171 fetchURL.searchParams.set(
172 'expandedRowIds',
173 expandedRowIds === 'all' ? 'all' : JSON.stringify(expandedRowIds ?? []),
174 );
175
176 //use whatever fetch library you want, fetch, axios, etc
177 const response = await fetch(fetchURL.href);
178 const json = (await response.json()) as UserApiResponse;
179 return json;
180 },
181 placeholderData: keepPreviousData, //don't go to 0 rows when refetching or paginating to next page
182 });
183};
184