How to Create an MUI DataGrid with Expandable Rows

Sometimes just because we can code something doesn’t mean we should use it in production. That’s how I feel about creating an expandable/collapsible row in the Material UI DataGrid.

In this tutorial, I added expand and collapse to the free MUI DataGrid (imported from "@mui/x-data-grid"). See the screenshot below:

Custom MUI DataGrid Expanded Rows Feature
Custom MUI DataGrid Expanded Rows Feature

This feature is only available on the Pro version of the MUI DataGrid…or maybe it will be soon according to this thread? The docs look like the feature is available, but I can’t be sure because I was previously fooled into thinking it existed in the free version…the getDetailPanelContent expansion prop seems to have been accidentally left in the docs on the free version:

MUI DataGrid Docs
MUI DataGrid Docs

Since the feature doesn’t exist in the free version of MUI, I quickly made a working demo of the feature. This has only been tested with fixed row heights. I recommend if you actually need the feature in a production app you should use the Pro version.

View a YouTube video version of this post or watch below:

Create an Awesome MUI DataGrid with...
Create an Awesome MUI DataGrid with Expandable Rows

MUI useState in the DataGrid

The first step to creating an expanded “detail” section in a DataGrid row is adding a column with a click listener.

I added a column that corresponded to the ID. It is primarily composed of a renderCell prop, which controls what elements and text render in the cell.

In this column I added an IconButton with a click listener. When the IconButton was clicked, the row index was saved to the clickedIndex useState value. If the row was already expanded, -1 was passed to setClickedIndex.

{
  field: "id",
  renderCell: (cellValues: GridRenderCellParams<number>) => {
    return (<IconButton onClick={() => {clickedIndex === cellValues.value ? setClickedIndex(-1) : setClickedIndex(cellValues.value)}}>{cellValues.value === clickedIndex ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}</IconButton>)
  },
  width: 60
}

The Icon inside the icon button was conditionally rendered based on expanded state to give a visual queue to the user.

A negative aspect of using a hook value in the column const is that I had to include the columns inside the functional component. This causes them to be recalculated with each render and is inefficient. However, there is likely a way to improve my code to get around this.

MUI Collapse Component in the DataGrid

In the renderCell of the remaining columns, I added a div with cellValues.value and a Collapse wrapping a Box with another cellValues.value. These will be vertically stacked in the DOM.

The Collapse in prop checks if the clickedIndex value is equal to the cellValues.id value. In larger DataGrids, this might require a lot of processing and may not be performant.

The code below is an example of one of the columns with Collapse. The others are almost the same. Since all cells in each row have the same id, they all expand together when the expand icon is clicked in the first column.

{
  field: "address",
  headerName: "Street/Appt Address",
  renderCell: (cellValues: GridRenderCellParams<string>) => {
    return (
      <Box><div>{cellValues.value}</div><Collapse in={cellValues.id === clickedIndex}><Box sx={detailStyles}>Expanded: {cellValues.value}</Box></Collapse></Box>
    )
  },
  width: 250
},

I added a top border and padding to the Box wrapped in the Collapse to give a visual queue that the section is expanded.

If you do not add cell borders on the left and right for each cell, then you can make the expanded section look like a single cell. However, this may be risky on dynamic screen sizes.

Resources

Here’s a tutorial for a typical DataGrid with filter, export, pagination, and more.

The Material UI DataGrid has lots of internal calculations for determining height, pagination, and more. That’s why I can’t recommend using this experimental code in a production setting.

Check out this tutorial where I added much needed edit and delete functionality to the MUI table.

Full code for this tutorial:

import * as React from "react";
import { Box, Collapse, IconButton, Paper, SxProps } from "@mui/material";
import { DataGrid, GridRenderCellParams, GridToolbar, GridColDef, GridToolbarContainer, GridToolbarExport, GridRowProps, GridRowParams } from "@mui/x-data-grid";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import { faker } from "@faker-js/faker";

const addresses: {
  [key: string]: any;
}[] = [];

for (let i = 0; i < 200; i++) {
  addresses.push({
      id: i+1,
    address: `${faker.address.streetAddress()} ${faker.address.secondaryAddress()}`,
    zip: faker.address.zipCode(),
    city: faker.address.city(),
    state: faker.address.state(),
  });
}

const datagridSx: SxProps = {
  marginTop: 4,
  borderRadius: 2,
  height: 500
  //minHeight: 500
};

const detailStyles={
  borderTop: "2px solid",
  borderTopColor: "primary.main",
  pt: 2
}

export default function ExpandedRowDataGrid() {
  const [clickedIndex, setClickedIndex] = React.useState(-1);

  const columns: GridColDef[] = [
    {
      field: "id",
      renderCell: (cellValues: GridRenderCellParams<number>) => {
          return (<IconButton onClick={() => {clickedIndex === cellValues.value ? setClickedIndex(-1) : setClickedIndex(cellValues.value)}}>{cellValues.value === clickedIndex ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}</IconButton>)
      },
      width: 60
    },
    {
      field: "address",
      headerName: "Street/Appt Address",
      renderCell: (cellValues: GridRenderCellParams<string>) => {
        return (
          <Box><div>{cellValues.value}</div><Collapse in={cellValues.id === clickedIndex}><Box sx={detailStyles}>Expanded: {cellValues.value}</Box></Collapse></Box>
        )
      },
      width: 250
    },
    { field: "zip", headerName: "Zip Code", flex: 1, editable: true,
    renderCell: (cellValues: GridRenderCellParams<string>) => {
      return (
        <Box><div>{cellValues.value}</div><Collapse in={cellValues.id === clickedIndex}><Box sx={detailStyles}>Expanded: {cellValues.value}</Box></Collapse></Box>
      )
    },
    width: 200
  },
    { field: "city", headerName: "City", flex: 1,
    renderCell: (cellValues: GridRenderCellParams<string>) => {
      return (
        <Box><div>{cellValues.value}</div><Collapse in={cellValues.id === clickedIndex}><Box sx={detailStyles}>Expanded: {cellValues.value}</Box></Collapse></Box>
      )
    },
    width: 200
  },
    { field: "state", headerName: "State", flex: 1, editable: true, 
    renderCell: (cellValues: GridRenderCellParams<string>) => {
      return (
        <Box><div>{cellValues.value}</div><Collapse in={cellValues.id === clickedIndex}><Box sx={detailStyles}>Expanded: {cellValues.value}</Box></Collapse></Box>
      )
    },
    width: 200
  }
  ];

  return (
    <Paper sx={datagridSx}>
      <DataGrid
        rows={addresses}
        columns={columns}
        rowHeight={100}
        pagination={true}
      />
    </Paper>
  );
}
Share this post:

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.