Aggregation and Grouping Feature Guide
Material React Table has built-in grouping and aggregation features. There are options for both automatic client-side grouping and aggregation, as well as manual server-side grouping and aggregation. This guide will walk you through the different options and how to use and customize them.
Relevant Props
# | Prop Name 2 | Type | Default Value | More Info Links | |
---|---|---|---|---|---|
1 |
| TanStack Table Grouping Docs | |||
2 |
|
| MRT Expanding Sub Rows Docs | ||
3 |
| MRT Aggregation and Grouping Docs | |||
4 |
| ||||
5 |
| TanStack Table Grouping Docs | |||
6 |
|
| TanStack Table Grouping Docs | ||
7 |
| TanStack Table Grouping Docs | |||
8 |
| Material UI Chip Props | |||
9 |
| TanStack Table Grouping Docs | |||
10 |
| ||||
Relevant Column Options
# | Column Option 2 | Type | Default Value | More Info Links | |
---|---|---|---|---|---|
1 |
| ||||
2 |
| MRT Data Columns Docs | |||
3 |
| ||||
4 |
| ||||
Relevant State
# | State Option | Type | Default Value | More Info Links | |
---|---|---|---|---|---|
1 |
|
| TanStack Table Expanding Docs | ||
2 |
|
| TanStack Table Grouping Docs | ||
Enable Grouping
To enable grouping, set the enableGrouping
prop to true
. This will both add a drag handle button so that columns can be dragged to the dropzone to be grouped and will add an entry column actions menu to group or ungroup a column.
<MaterialReactTable columns={columns} data={data} enableGrouping />
Disable Grouping Per Column
const columns = [{accessorKey: 'name',header: 'Name',enableGrouping: false, // disable grouping for this column},{accessorKey: 'age',header: 'Age',},];return <MaterialReactTable columns={columns} data={data} enableGrouping />;
Hide Drag Buttons for Grouping
If you do not want the drag buttons that come with the grouping feature, you can independently disable them without disabling the grouping feature entirely by setting the enableColumnDragging
prop to false
.
<MaterialReactTablecolumns={columns}data={data}enableGroupingenableColumnDragging={false} //do not show drag handle buttons, but still show grouping options in column actions menu/>
Group Columns by Default
If you want columns to be grouped by default, you can set the grouping
state in either the initialState
or state
prop.
<MaterialReactTablecolumns={columns}data={data}enableGroupinginitialState={{ grouping: ['location', 'department'] }} //group by location and department by default/>
Expand Grouped Rows by Default
In addition to grouping columns by default, you may also want those grouped rows to be expanded and visible by default, too. You can do this by setting the expanded
state to true
in either the initialState
or state
prop.
<MaterialReactTablecolumns={columns}data={data}enableGroupinginitialState={{grouping: ['location', 'department'], //group by location and department by default and expand grouped rowsexpanded: true, //show grouped rows by default}}/>
Aggregation on Grouped Rows
One of the cool features of Material React Table is that it can automatically aggregate the data in grouped rows. To enable this, you must specify both an aggregationFn
and an AggregatedCell
render option on a column definition.
Built-in Aggregation Functions
There are several built-in aggregation functions available that you can use. They are:
count
- Finds the number of rows in a groupextent
- Finds the minimum and maximum values of a group of rowsmax
- Finds the maximum value of a group of rowsmean
- Finds the average value of a group of rowsmedian
- Finds the median value of a group of rowsmin
- Finds the minimum value of a group of rowssum
- sums the values of a group of rowsuniqueCount
- Finds the number of unique values of a group of rowsunique
- Finds the unique values of a group of rows
All of these built-in aggregation functions are from TanStack Table
const columns = [{accessorKey: 'team', //grouped by team in initial state belowheader: 'Team',},{accessorKey: 'player',header: 'Player',},{accessorKey: 'points',header: 'Points',aggregationFn: 'sum', //calc total points for each team by adding up all the points for each player on the teamAggregatedCell: ({ cell }) => <div>Team Score: {cell.getValue()}</div>,},];return (<MaterialReactTablecolumns={columns}data={data}enableGroupinginitialState={{ grouping: ['team'], expanded: true }}/>);
Custom Aggregation Functions
If none of these pre-built aggregation functions work for you, you can also pass in a custom aggregation function. The aggregation function will be passed in an array of values from the column that you are aggregating. It should return a single value that will be displayed in the aggregated cell.
If you are specifying a custom aggregation function, it must implement the following type:
export type AggregationFn<TData extends AnyData> = (getLeafRows: () => Row<TData>[],getChildRows: () => Row<TData>[]) => any
Aggregation on All Rows in Footer
Material React Table does not automatically aggregate all rows for you to calculate totals for the entire table. However, it is still easy enough to do this manually and add in your custom calculations into the footer
or Footer
of a column definition. It is recommended that you do any necessary aggregation calculations on your data in a useMemo hook before passing it to the columns footer in your columns definition.
//calculate the total points for all players in the table in a useMemo hookconst averageScore = useMemo(() => {const totalPoints = data.reduce((acc, row) => acc + row.points, 0);const totalPlayers = data.length;return totalPoints / totalPlayers;}, [data]);const columns = [{accessorKey: 'name',header: 'Name',},{accessorKey: 'score',header: 'Score',Footer: () => <div>Average Score: {averageScore}</div>, //do not do calculations in render, do them in useMemo hook and pass them in here},];
Please remember to perform heavy aggregation calculations in a useMemo hook to avoid unnecessary re-renders!
Custom Cell Renders for Aggregation and Grouping
There are a few custom cell render overrides that you should be aware of when using grouping and aggregation features.
AggregatedCell Column Option
"Aggregation Cells" are cells in an aggregated row (not a normal data row) that can display aggregates (avg, sum, etc.) of the data in a group. The cell that the table is grouped on, however, is not an Aggregate Cell, but rather a GroupedCell.
You can specify the custom render for these cells with the AggregatedCell
render option on a column definition.
const columns = [{accessorKey: 'points',header: 'Points',aggregationFn: 'sum',AggregatedCell: ({ cell }) => <div>Total Score: {cell.getValue()}</div>,},];
GroupedCell Column Option
"Grouped Cells" are cells in a grouped row (not a normal data row) that by default display the value that the rows are grouped on and the number of rows in the group. You can override the default render for these cells with the GroupedCell
render option on a column definition.
const columns = [{accessorKey: 'team',header: 'Team',GroupedCell: ({ cell }) => <div>Team: {cell.getValue()}</div>,},];
PlaceholderCell Column Option
New in v1.7.4
"Placeholder Cells" are cells that are usually meant to be empty in grouped rows and columns. They are simply rendered with a value of null
by default, but you can override the default render for these cells with the PlaceholderCell
render option on a column definition.
const columns = [{accessorKey: 'team',header: 'Team',PlaceholderCell: ({ cell, row }) => (<div>{row.original.someOtherRowValue}</div>),},];
Aggregation/Grouping Example
State | First Name | Last Name | Age | Gender | Salary | |
---|---|---|---|---|---|---|
Alabama (7) | Oldest by State: 64 | Average by State: $43,375 | ||||
Thad | Wiegand | 64 | Female | $56,146 | ||
Alivia | Ledner | 56 | Male | $12,591 | ||
Danyka | Gleason | 36 | Male | $71,238 | ||
Lionel | Hartmann | 30 | Nonbinary | $58,743 | ||
Reinhold | Reichel | 30 | Female | $30,531 | ||
Lurline | Koepp | 59 | Female | $10,645 | ||
Kody | Braun | 38 | Female | $63,733 | ||
Alaska (8) | Oldest by State: 59 | Average by State: $68,901 | ||||
Eloisa | Kohler | 31 | Male | $45,801 | ||
Kian | Hand | 56 | Male | $81,062 | ||
Loyce | Schmidt | 29 | Female | $76,295 | ||
Michale | Collier | 59 | Male | $75,197 | ||
Eldridge | Stroman | 42 | Male | $59,594 | ||
Alvera | Balistreri | 25 | Female | $79,844 | ||
Kayden | Emard | 35 | Female | $98,252 | ||
Domingo | Bauch | 36 | Female | $35,159 | ||
Arizona (1) | Oldest by State: 22 | Average by State: $54,027 | ||||
Gunner | Rolfson | 22 | Male | $54,027 | ||
Arkansas (4) | Oldest by State: 52 | Average by State: $58,194 | ||||
Max Age: 65 | Average Salary: $56,319 |
1import React, { useMemo } from 'react';2import { Box, Stack } from '@mui/material';3import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table';4import { data, type Person } from './makeData';56const Example = () => {7 const averageSalary = useMemo(8 () => data.reduce((acc, curr) => acc + curr.salary, 0) / data.length,9 [],10 );1112 const maxAge = useMemo(13 () => data.reduce((acc, curr) => Math.max(acc, curr.age), 0),14 [],15 );1617 const columns = useMemo<MRT_ColumnDef<Person>[]>(18 () => [19 {20 header: 'First Name',21 accessorKey: 'firstName',22 enableGrouping: false, //do not let this column be grouped23 },24 {25 header: 'Last Name',26 accessorKey: 'lastName',27 },28 {29 header: 'Age',30 accessorKey: 'age',31 aggregationFn: 'max', //show the max age in the group (lots of pre-built aggregationFns to choose from)32 //required to render an aggregated cell33 AggregatedCell: ({ cell, table }) => (34 <>35 Oldest by{' '}36 {table.getColumn(cell.row.groupingColumnId ?? '').columnDef.header}:{' '}37 <Box38 sx={{ color: 'info.main', display: 'inline', fontWeight: 'bold' }}39 >40 {cell.getValue<number>()}41 </Box>42 </>43 ),44 Footer: () => (45 <Stack>46 Max Age:47 <Box color="warning.main">{Math.round(maxAge)}</Box>48 </Stack>49 ),50 },51 {52 header: 'Gender',53 accessorKey: 'gender',54 //optionally, customize the cell render when this column is grouped. Make the text blue and pluralize the word55 GroupedCell: ({ cell, row }) => (56 <Box sx={{ color: 'primary.main' }}>57 <strong>{cell.getValue<string>()}s </strong> ({row.subRows?.length})58 </Box>59 ),60 },61 {62 header: 'State',63 accessorKey: 'state',64 },65 {66 header: 'Salary',67 accessorKey: 'salary',68 aggregationFn: 'mean',69 //required to render an aggregated cell, show the average salary in the group70 AggregatedCell: ({ cell, table }) => (71 <>72 Average by{' '}73 {table.getColumn(cell.row.groupingColumnId ?? '').columnDef.header}:{' '}74 <Box sx={{ color: 'success.main', fontWeight: 'bold' }}>75 {cell.getValue<number>()?.toLocaleString?.('en-US', {76 style: 'currency',77 currency: 'USD',78 minimumFractionDigits: 0,79 maximumFractionDigits: 0,80 })}81 </Box>82 </>83 ),84 //customize normal cell render on normal non-aggregated rows85 Cell: ({ cell }) => (86 <>87 {cell.getValue<number>()?.toLocaleString?.('en-US', {88 style: 'currency',89 currency: 'USD',90 minimumFractionDigits: 0,91 maximumFractionDigits: 0,92 })}93 </>94 ),95 Footer: () => (96 <Stack>97 Average Salary:98 <Box color="warning.main">99 {averageSalary?.toLocaleString?.('en-US', {100 style: 'currency',101 currency: 'USD',102 minimumFractionDigits: 0,103 maximumFractionDigits: 0,104 })}105 </Box>106 </Stack>107 ),108 },109 ],110 [averageSalary, maxAge],111 );112113 return (114 <MaterialReactTable115 columns={columns}116 data={data}117 enableColumnResizing118 enableGrouping119 enableStickyHeader120 enableStickyFooter121 initialState={{122 density: 'compact',123 expanded: true, //expand all groups by default124 grouping: ['state'], //an array of columns to group by by default (can be multiple)125 pagination: { pageIndex: 0, pageSize: 20 },126 sorting: [{ id: 'state', desc: false }], //sort by state by default127 }}128 muiToolbarAlertBannerChipProps={{ color: 'primary' }}129 muiTableContainerProps={{ sx: { maxHeight: 700 } }}130 />131 );132};133134export default Example;135
Multiple Aggregations Per column
New in v1.3!
You may want to calculate more than one aggregation per column. This is now easier if you are upgraded to at least v1.3.0. You can now specify an array of aggregationFn
s, and then reference the aggregation results from an array in the AggregatedCell
render option.
const columns = [{header: 'Salary',accessorKey: 'salary',aggregationFn: ['count', 'mean'], //multiple aggregation functionsAggregatedCell: ({ cell, table }) => (<div>{/*get the count from the first aggregation*/}<div>Count: {cell.getValue()[0]}</div>{/*get the average from the second aggregation*/}<div>Average Salary: {cell.getValue()[1]}</div></div>),},];
State | Gender | First Name | Last Name | Salary | |
---|---|---|---|---|---|
Alabama (3) | Count: 7 Average: $43,375 Median: $56,146 Min: $10,645 Max: $71,238 | ||||
Female (4) | Count: 4 Average: $40,264 Median: $43,339 Min: $10,645 Max: $63,733 | ||||
Thad | Wiegand | $56,146 | |||
Reinhold | Reichel | $30,531 | |||
Lurline | Koepp | $10,645 | |||
Kody | Braun | $63,733 | |||
Male (2) | Count: 2 Average: $41,915 Median: $41,915 Min: $12,591 Max: $71,238 | ||||
Alivia | Ledner | $12,591 | |||
Danyka | Gleason | $71,238 | |||
Nonbinary (1) | Count: 1 Average: $58,743 Median: $58,743 Min: $58,743 Max: $58,743 | ||||
Lionel | Hartmann | $58,743 | |||
Alaska (2) | Count: 8 Average: $68,901 Median: $75,746 Min: $35,159 Max: $98,252 | ||||
Male (4) | Count: 4 Average: $65,414 Median: $67,396 Min: $45,801 Max: $81,062 | ||||
Eloisa | Kohler | $45,801 | |||
Kian | Hand | $81,062 | |||
Michale | Collier | $75,197 | |||
Eldridge | Stroman | $59,594 | |||
Female (4) | Count: 4 Average: $72,388 Median: $78,070 Min: $35,159 Max: $98,252 | ||||
Loyce | Schmidt | $76,295 | |||
Alvera | Balistreri | $79,844 |
1import React, { useMemo } from 'react';2import { Box } from '@mui/material';3import { MaterialReactTable, type MRT_ColumnDef } from 'material-react-table';4import { data, type Person } from './makeData';56const localeStringOptions = {7 style: 'currency',8 currency: 'USD',9 minimumFractionDigits: 0,10 maximumFractionDigits: 0,11};1213const Example = () => {14 const columns = useMemo<MRT_ColumnDef<Person>[]>(15 () => [16 {17 header: 'First Name',18 accessorKey: 'firstName',19 },20 {21 header: 'Last Name',22 accessorKey: 'lastName',23 },24 {25 header: 'Gender',26 accessorKey: 'gender',27 },28 {29 header: 'State',30 accessorKey: 'state',31 },32 {33 header: 'Salary',34 accessorKey: 'salary',35 aggregationFn: ['count', 'mean', 'median', 'min', 'max'],36 //required to render an aggregated cell, show the average salary in the group37 AggregatedCell: ({ cell }) => (38 <>39 Count:{' '}40 <Box sx={{ color: 'success.main', fontWeight: 'bold' }}>41 {cell.getValue<Array<number>>()?.[0]}42 </Box>43 Average:{' '}44 <Box sx={{ color: 'success.main', fontWeight: 'bold' }}>45 {cell46 .getValue<Array<number>>()?.[1]47 ?.toLocaleString?.('en-US', localeStringOptions)}48 </Box>49 Median:{' '}50 <Box sx={{ color: 'success.main', fontWeight: 'bold' }}>51 {cell52 .getValue<Array<number>>()?.[2]53 ?.toLocaleString?.('en-US', localeStringOptions)}54 </Box>55 Min:{' '}56 <Box sx={{ color: 'success.main', fontWeight: 'bold' }}>57 {cell58 .getValue<Array<number>>()?.[3]59 ?.toLocaleString?.('en-US', localeStringOptions)}60 </Box>61 Max:{' '}62 <Box sx={{ color: 'success.main', fontWeight: 'bold' }}>63 {cell64 .getValue<Array<number>>()?.[4]65 ?.toLocaleString?.('en-US', localeStringOptions)}66 </Box>67 </>68 ),69 //customize normal cell render on normal non-aggregated rows70 Cell: ({ cell }) => (71 <>72 {cell73 .getValue<number>()74 ?.toLocaleString?.('en-US', localeStringOptions)}75 </>76 ),77 },78 ],79 [],80 );8182 return (83 <MaterialReactTable84 columns={columns}85 data={data}86 enableGrouping87 enableStickyHeader88 initialState={{89 density: 'compact',90 expanded: true, //expand all groups by default91 grouping: ['state', 'gender'], //an array of columns to group by by default (can be multiple)92 pagination: { pageIndex: 0, pageSize: 20 },93 sorting: [{ id: 'state', desc: false }], //sort by state by default94 }}95 muiToolbarAlertBannerChipProps={{ color: 'primary' }}96 muiTableContainerProps={{ sx: { maxHeight: 700 } }}97 />98 );99};100101export default Example;102
Manual Grouping
Manual Grouping means that the data
that you pass to the table is already grouped and aggregated, and you do not want Material React Table to do any of the grouping or aggregation for you. This is useful if you are using a backend API to do the grouping and aggregation for you, and you just want to display the results. However, you will need to put your data in the specific format that the expanding
features understand.