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:

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:

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:

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>
);
}