How to Create a MUI Table with Edit and Delete Rows Feature

The MUI Table can quickly be enhanced with edit and delete functionality. A good approach is to use state management, perhaps even useState, and click listeners to swap a TextField into a Cell and capture user typing. Deleting requires a component column and another useState value.

Here’s what we will build:

MUI Table with Edit on Cell Click and Delete
MUI Table with Edit on Cell Click and Delete

Notice there is no “Edit” button. Instead, I designed the table so that cells become editable after they are clicked into.

Full code for this tutorial is in the Resources section.

How to Edit MUI Table Rows on Click

We can edit a row on click by rendering a TextField instead of simple text when a TableCell is clicked.

First we need to track the row and ‘column’ that are clicked using two state values:

const [rowIndex, setRowIndex] = React.useState(-1);
const [columnIndex, setColumnIndex] = React.useState(-1);

I set these to -1 so that no cell would start with a TextField.

Next, we need to add an onClick handler to each TableCell that needs to be editable. In my example I only added it to the first cell in each row. This click handler simply sets the two state values with the most recent clicked cell indexes.

{rows.map((row, index) => (
<TableRow key={row.name}>
  <TableCell
    onClick={() => { setRowIndex(index); setColumnIndex(0); }}
    sx={tableBodyStyling}
  >
    {
      rowIndex === index && columnIndex === 0 ?
        <TextField
          placeholder={row.name}
          defaultValue={rows[index]["name"]}
          onChange={(event) => handleTextFieldChange(index, "name", event.target.value)}
        /> : row.name
    }
  </TableCell>
  //More table cells
</TableRow>
)

In the TableCell child code, we render either the relevant data for that cell, or we render a TextField if the row and column index match the values in our state managers. The TextField has an onChange handler that simply sets the new value in the rows array.

  const handleTextFieldChange = (rowInd: number, colName: "name" | "calories" | "fat" | "carbs" | "protein", value: string) => {
    rows[rowInd][colName] = value;
  };

Here’s what the table looks like when an edit is in progress:

MUI Table with Cell Being Edited
MUI Table with Cell Being Edited

How to Save MUI Table Row Data on Click Away or Enter Press

When the user clicks outside the table or presses enter after editing a cell, we want the table to render the new data without a TextField in the cell. We do this by informing our state values that no index is currently selected.

To handle the re-render on click away, we need to wrap the entire table in a MUI ClickAwayListener. The ClickAwayLister simplifies handling of clicks outside of a component or area. We can populate the onClickAway handler with a function called handleExit, which we will explore below.

<ClickAwayListener onClickAway={() => handleExit()}>
  <TableContainer component={Paper} >
    //Table Subcomponents

Next, we need to add the same handler to the TextField component so it “knows” to re-render when Enter is pressed:

onKeyPress={(e) => {
  if (e.key === "Enter") {
    handleExit();
  }
}}

Finally, we need to create the handleExit function. The data changes are already being saved by the handleTextFieldChange function, so the handleExit function only needs to inform the state managers that no cells are currently being edited:

const handleExit = () => {
  setRowIndex(-1);
  setColumnIndex(-1);
}

With this, our Table edit functionality is complete.

How to Delete a Row of Data in a MUI Table

In order to delete a row, we need two features in place:

  • The table data is in an observable state value
  • A delete button exists for each row and modifies the state value on click

We need the data in a state value (using Redux, MobX, or simply React.useState) so that the Table re-renders when a row of data is removed. Here’s an example using useState and a value called rows:

  const [rows, setRows] = React.useState<Array<Food>>([
    { name: "Donut", calories: "159", fat: "6.0", carbs: "24", protein: "4.0" },

    //...more data
  ]);

Next we need a “column” in the Table that contains our Delete button. The onClick handler needs to remove the element from our rows data that corresponds to the index of the clicked Delete button’s row. We can use Array.splice for this:

<TableCell sx={tableBodyStyling}>
  <Button 
    variant="contained" 
    onClick={() => { rows.splice(index, 1); 
    setRows([...rows]); }}
  >
    Delete
  </Button>
</TableCell>

Don’t forget to call setRows and use the spread operator on rows. This is necessary to trigger an observable change in the rows data so the table re-renders.

Here’s the same table after deleting several rows:

MUI Table After Row Delete
MUI Table After Row Delete

Resources

I also hacked the MUI DataGrid and added expandable rows to it.

Here is full code for this tutorial:

import React from "react";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";

interface Food {
  name: string,
  calories: string,
  fat: string,
  carbs: string,
  protein: string
}

const tableHeaderStyling = {
  padding: "0px 0px",
  fontSize: 24,
  borderBottom: "1px solid",
  borderBottomColor: "#1976d2",
  backgroundColor: "primary.main"
};

const tableBodyStyling = {
  backgroundColor: "success.light",
  color: "secondary.dark",
  fontSize: 20
}

export default function EditDeleteTable() {
  const [rowIndex, setRowIndex] = React.useState(-1);
  const [columnIndex, setColumnIndex] = React.useState(-1);
  const [rows, setRows] = React.useState<Array<Food>>([
    { name: "Donut", calories: "159", fat: "6.0", carbs: "24", protein: "4.0" },
    { name: "Hot Dog", calories: "237", fat: "9.0", carbs: "37", protein: "4.3" },
    { name: "Pizza", calories: "262", fat: "16.0", carbs: "24", protein: "6.0" },
    { name: "Pie", calories: "305", fat: "3.7", carbs: "67", protein: "4.3" },
    { name: "Sandwich", calories: "356", fat: "16.0", carbs: "49", protein: "3.9" }
  ]);

  const handleTextFieldChange = (rowInd: number, colName: "name" | "calories" | "fat" | "carbs" | "protein", value: string) => {
    rows[rowInd][colName] = value;
  };

  const handleExit = () => {
    setRowIndex(-1);
    setColumnIndex(-1);
  }

  return (
    <ClickAwayListener onClickAway={() => handleExit()}>
      <TableContainer
        component={Paper}
      >
        <Table sx={{ tableLayout: "auto" }}>
          <TableHead>
            <TableRow>
              <TableCell sx={{ ...tableHeaderStyling, width: 100 }}>
                Food
              </TableCell>
              <TableCell sx={{ ...tableHeaderStyling, width: 100 }}>
                Calories
              </TableCell>
              <TableCell sx={{ ...tableHeaderStyling, width: 100 }}>
                Fat
              </TableCell>
              <TableCell sx={{ ...tableHeaderStyling, width: 100 }}>
                Carbs
              </TableCell>
              <TableCell sx={{ ...tableHeaderStyling, width: 100 }}>
                Protein
              </TableCell>
              <TableCell sx={{ ...tableHeaderStyling, width: 100 }}>
                Remove Column
              </TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {rows.map((row, index) => (
              <TableRow key={row.name}>
                <TableCell
                  onClick={() => { setRowIndex(index); setColumnIndex(0); }}
                  sx={tableBodyStyling}
                >
                  {
                    rowIndex === index && columnIndex === 0 ?
                      <TextField
                        placeholder={row.name}
                        defaultValue={rows[index]["name"]}
                        onChange={(event) => handleTextFieldChange(index, "name", event.target.value)}
                        onKeyPress={(e) => {
                          if (e.key === "Enter") {
                            handleExit();
                          }
                        }}
                      /> : row.name
                  }
                </TableCell>
                <TableCell
                  sx={tableBodyStyling}
                >
                  {row.calories}
                </TableCell>
                <TableCell
                  sx={tableBodyStyling}
                >
                  {row.fat}
                </TableCell>
                <TableCell
                  sx={tableBodyStyling}
                >
                  {row.carbs}
                </TableCell>
                <TableCell
                  sx={tableBodyStyling}
                >
                  {row.protein}
                </TableCell>
                <TableCell sx={tableBodyStyling}><Button variant="contained" onClick={() => { rows.splice(index, 1); setRows([...rows]); }}>Delete</Button></TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </TableContainer>
    </ClickAwayListener>
  );
}
Share this post:

Leave a Comment

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