Guide to Editing, Deleting, and Updating Data in MUI X DataGrid

Guide to Editing, Deleting, and Updating Data in MUI X DataGrid

·

14 min read

Hey there! How’s it going? 😊
Hope all is well with you! In today’s blog, we’re diving into the customizations of tabular data using the powerful tools provided by MUI X DataGrid.

When it comes to working with tables, there are a few essential operations—like editing, deleting, and updating—that can make a big difference in user experience. The good news? MUI X DataGrid simplifies these tasks for us by providing built-in tools to handle them seamlessly.

But that’s not all! MUI X DataGrid is incredibly flexible—it’s not just a rigid data structure. You can take advantage of its robust set of features, or even extend its functionality to meet your specific needs. Whether your table is small or scales to thousands of rows, this library can handle it effortlessly.

Installation

This blog assumes that you have already set up React and installed the necessary dependencies. We will pick up from there and dive straight into the core features of MUI X Data Grid.

Lets start by installing data grid.

npm install @mui/x-data-grid

Step 1: Set Up Sample Data

Before we dive into the operations, we need some sample data to work with. Below is a sample dataset you can use. Save this dataset in a separate folder, for example, a data folder in your project directory:

// data/products.js
const sampleData = [
    { "id": 1, "productName": "High-End Gaming Laptop with 16GB RAM and 1TB SSD", "category": "Electronics", "price": 1499.99, "stock": 12, "rating": 4.8, "supplier": "Elite Tech Solutions" }
    { "id": 3, "productName": "Noise-Canceling Over-Ear Bluetooth Headphones with Superior Sound Quality", "category": "Audio", "price": 249.99, "stock": 40, "rating": 4.7, "supplier": "SoundX Audio" }
    { "id": 4, "productName": "Mechanical RGB Gaming Keyboard with Customizable Keys and LED Lighting", "category": "Accessories", "price": 129.99, "stock": 25, "rating": 4.4, "supplier": "TechSavvy Accessories" }
    { "id": 5, "productName": "Ergonomic Wireless Mouse with Adjustable DPI for Precision Control", "category": "Accessories", "price": 49.99, "stock": 70, "rating": 4.3, "supplier": "Precision Devices" }
    { "id": 6, "productName": "Smart Fitness Tracker with Heart Rate Monitoring and Sleep Tracking Features", "category": "Wearables", "price": 199.99, "stock": 35, "rating": 4.8, "supplier": "FitLife Wearables" }
    { "id": 7, "productName": "10.1 Inch Android Tablet with High Resolution Display and Long Battery Life", "category": "Electronics", "price": 349.99, "stock": 20, "rating": 4.5, "supplier": "TabWorld" }
    { "id": 8, "productName": "Ultra-Wide 34 Inch Curved Monitor with 144Hz Refresh Rate for Gamers", "category": "Electronics", "price": 349.99, "stock": 15, "rating": 4.6, "supplier": "VisionTech" }
    { "id": 9, "productName": "Portable Bluetooth Speaker with Waterproof Design and 12 Hour Battery Life", "category": "Audio", "price": 119.99, "stock": 50, "rating": 4.7, "supplier": "SoundBliss" }
    { "id": 10, "productName": "1TB External Hard Drive with Fast Data Transfer and Backup Solutions", "category": "Storage", "price": 139.99, "stock": 30, "rating": 4.4, "supplier": "StoragePro" }
];

Step 2: Install React-Toastify for Error Validation

To handle error validations and display user-friendly notifications, we’ll use the React-Toastify library. This library is a great choice for showing toast messages in a sleek and customizable way.

npm install react-toastify

Step 3: Setting Up React-Toastify in the Main App.js File

Now that we’ve installed React-Toastify, let’s configure it in the App.js file. We’ll define toast notifications for four states: warning, error, success, and info, which will cover various future use cases.

React-Toastify provides customizable props, allowing you to fine-tune the appearance and behavior of the notifications. Personally, I prefer placing the toasts in the top-left corner for better visibility. Additionally, this file will also define and render the Table.jsx component. MUI’s palette colors are used for the toastr colors.

Implementation

Here’s how you can set it up in App.js:

import React from "react";
import { Box, CssBaseline, useTheme } from "@mui/material";
import Table from "./Table";
import { fillParent, footerFlex, scrollingBox } from "./utils/baseStyles.util";
import {
  CheckOutlined,
  ErrorOutline,
  InfoOutlined,
  WarningOutlined,
} from "@mui/icons-material";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css"; 

function App() {
  const { palette } = useTheme();

  return (
    <React.Fragment>
      <ToastContainer
        position="top-left"
        autoClose={5000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        pauseOnFocusLoss
        draggable
        pauseOnHover
        icon={({ type }) => {
          switch (type) {
            case "success":
              return <CheckOutlined sx={{ color: palette.success.main }} />;
            case "info":
              return <InfoOutlined sx={{ color: palette.info.main }} />;
            case "warning":
              return <WarningOutlined sx={{ color: palette.warning.main }} />;
            case "error":
              return <ErrorOutline sx={{ color: palette.error.main }} />;
            default:
              return false;
          }
        }}
        theme="light"
      />

      <CssBaseline enableColorScheme />
      <Box
        sx={{
          overflowY: { xs: "unset", sm: "auto" },
          p: 3,
        }}
      >
        <Table />
      </Box>
    </React.Fragment>
  );
}

export default App;

Step 4: Setting Up a Basic DataGrid Table

To get started with MUI X DataGrid, we need to set up a basic table. This provides us with a visual foundation and some initial functionality, which we’ll enhance with advanced features in later steps.

Here’s a simple implementation of the Table.jsx component:

import React, { useState } from "react";
import { DataGrid } from "@mui/x-data-grid";
import { Box, Typography } from "@mui/material";
import { rows } from "./data/products";

function DataTable() {
    const [paginationModel, setPaginationModel] = useState({
    page: 0,
    pageSize: 5, 
    });

  const sx = {
    display: "flex",
    alignItems: "center",
    height: "100%",
    cursor: "pointer",
  };

const columns = [
  { field: "id", headerName: "ID", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body2">{params.value}</Typography> },
  { field: "productName", headerName: "Product Name", flex: 3, renderCell: (params) => <Typography sx={sx} variant="body2">{params.value}</Typography> },
  { field: "category", headerName: "Category", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body2">{params.value}</Typography> },
  { field: "price", headerName: "Price ($)", type: "number", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body2">{params.value}</Typography> },
  { field: "stock", headerName: "Stock", type: "number", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body2">{params.value}</Typography> },
  { field: "rating", headerName: "Rating", type: "number", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body2">{params.value}</Typography> },
  { field: "supplier", headerName: "Supplier", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body2">{params.value}</Typography> },
];
  return (
    <Box sx={{ width: "100%" }}>
      <Typography variant="h6" mb={2}>
        Table Content
      </Typography>
      <DataGrid
        rows={rows}
        columns={columns}
        pageSize={5}
        sx={{
          border: "none",
          width: "100%",
        }}
        getRowId={(row) => row.id}
        paginationModel={paginationModel}
        onPaginationModelChange={(model) => setPaginationModel(model)}
        pageSizeOptions={[5, 10, 15, 20]}
      />
    </Box>
  );
}

export default DataTable;

Step 5: Editing and saving

To enable row editing in the DataGrid, we’ll add an additional "Actions" column containing buttons (Edit and Save). This column allows users to toggle a row between view and edit modes.

Steps for Row Editing:

  1. Editable Columns:

    • To make a column editable, include editable: true in the column’s configuration. This allows users to modify the cell content when the row is in edit mode.
  2. Row State Management:

    • We'll use rowModesModel to track which row is in edit mode.

    • This state determines whether the row is editable or in view-only mode.

  3. UI Interaction:

    • When the Edit button is clicked, the corresponding row switches to edit mode, enabling the user to modify its values.

    • After making changes, clicking the Save button updates the row data and switches it back to view mode.

Implementation of State Switching:

We will now implement the row-switching logic for edit and save. The rowModesModel will track the state of each row. Additionally, we’ll use processRowUpdate to update the data when the Save button is clicked.

// Other imports
import React, { useState } from "react";
import { DataGrid } from "@mui/x-data-grid";
import { rows as SampleData } from "./data/products";
import EditIcon from "@mui/icons-material/Edit";
import SaveIcon from "@mui/icons-material/Save";
import CancelIcon from "@mui/icons-material/Cancel";
import { Typography } from "@mui/material";

function DataTable() {
  const [rowModesModel, setRowModesModel] = useState({});
  const [rows, setRows] = useState(SampleData);

  const handleEditClick = (id) => {
    setRowModesModel((prevModel) => ({
      ...prevModel,
      [id]: { mode: "edit" },
    }));
  };

  const handleSaveClick = (id) => {
    setRowModesModel((prevModel) => ({
      ...prevModel,
      [id]: { mode: "view" },
    }));
  };

  const handleCancelClick = (id) => {
    setRowModesModel((prevModel) => ({
      ...prevModel,
      [id]: { mode: "view", ignoreModifications: true },
    }));
  };

  const sx = {
    display: "flex",
    alignItems: "center",
    height: "100%",
    cursor: "pointer",
  };

  const columns = [
    // Other column definitions...
    {
      field: "supplier",
      headerName: "Supplier",
      editable: true,
      flex: 1,
      renderCell: (params) => (
        <Typography sx={sx} noWrap variant="body2">
          {params.value}
        </Typography>
      ),
    },
    {
      field: "actions",
      type: "actions",
      headerName: "Actions",
      flex: 1,
      getActions: ({ id }) => {
        const isInEditMode = rowModesModel[id]?.mode === "edit";

        if (isInEditMode) {
          return [
            <SaveIcon
              key={`save-${id}`}
              sx={{ fontSize: "2rem", cursor: "pointer", mr: 1 }}
              onClick={() => handleSaveClick(id)}
            />,
            <CancelIcon
              key={`cancel-${id}`}
              sx={{ fontSize: "2rem", cursor: "pointer" }}
              onClick={() => handleCancelClick(id)}
            />,
          ];
        }
        return [
          <EditIcon
            key={`edit-${id}`}
            sx={{ fontSize: "2rem", cursor: "pointer" }}
            onClick={() => handleEditClick(id)}
          />,
        ];
      },
    },
  ];

  return (
    <Box sx={{ width: "100%" }}>
      <Typography variant="h6" mb={2}>
        Table Content
      </Typography>
      <DataGrid
        rows={rows}
        columns={columns}
        pageSize={5}
        sx={{
          border: "none",
          width: "100%",
        }}
        getRowId={(row) => row.id}
        paginationModel={paginationModel}
        onPaginationModelChange={(model) => setPaginationModel(model)}
        pageSizeOptions={[5, 10, 15, 20]}
        rowModesModel={rowModesModel}
      />
    </Box>
  );
}

export default DataTable;

Step 6: Handling Row Updates with Validations in MUI DataGrid

To handle row updates in the MUI DataGrid, we’ll use the processRowUpdate function. This function provides us with the updated row data, allowing us to perform validations before committing the changes to the state.

However, to ensure that the updated data is valid, we need to implement error handling for scenarios where validation fails. This is where handleProcessRowUpdateError comes into play, allowing us to display error messages when necessary.

Additionally, we’ll be using react-toastify (via toastr) to notify users about the success or failure of their updates.

Steps to Handle Row Updates:

  1. processRowUpdate Function:

    • The processRowUpdate function is triggered whenever a user modifies a cell. It returns the updated row, which we can use to update the table's state. However, before we update the state, we can validate the data.
  2. handleProcessRowUpdateError for Error Handling:

    • In case any validation fails during the row update, we can handle errors using handleProcessRowUpdateError. This function allows us to stop the update and display an error message if the validation does not pass.
  3. Displaying Notifications with Toastr:

    • We’ll use react-toastify to show success or error messages to users. For example, when a row update is successful, we’ll display a success toast, and if validation fails, we’ll display an error toast.

import { toast } from "react-toastify";

function DataTable() {
   .............

     const validateRow = (newRow) => {
       const errors = {};

       if (newRow.price <= 0) {
         errors.price = "Price must be positive";
       }

       if (newRow.rating <= 0) {
         errors.rating = "Rating must be positive";
       }

       if (newRow.stock < 0) {
         errors.stock = "Stock cannot be negative";
       }

       if (newRow.productName.length <= 0) {
         errors.productName = "Product name cannot be empty!";
       }

       return errors;
     };

     const handleProcessRowUpdateError = (error) => {
       const errorDetails = JSON.parse(error.message);
       console.error("Row validation errors:", errorDetails);
       toast.error(
         `Validation Errors: ${Object.values(errorDetails).join(", ")}`
       );
     };

     const processRowUpdate = (newRow) => {
       const errors = validateRow(newRow);
       const updatedRow = { ...newRow, lastUpdated: new Date() };
       if (Object.keys(errors).length > 0) {
         throw new Error(JSON.stringify(errors));
       }
       setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row)));
       toast.success("Row updated successfully!")
       return updatedRow;
     };

........

    <DataGrid
        rows={rows}
        columns={columns}
        pageSize={5}
        sx={{
          border: "none",
          width: "100%",
        }}
        getRowId={(row) => row.id}
        paginationModel={paginationModel}
        onPaginationModelChange={(model) => setPaginationModel(model)}
        pageSizeOptions={[5, 10, 15, 20]}
        processRowUpdate={processRowUpdate}
        onProcessRowUpdateError={handleProcessRowUpdateError}
        rowModesModel={rowModesModel}
      />

};

Step 6: Handling Row Deletes

When working with tables, the ability to perform bulk actions—such as deleting multiple rows at once—is a crucial feature. With MUI's DataGrid, you can easily enable row selection and manage bulk deletions using checkboxes and custom toolbar actions. Let’s break down how to implement this functionality step by step.

Step 1: Enable Checkbox Selection

To allow users to select multiple rows, we can use the checkboxSelection prop in the DataGrid. This adds checkboxes next to each row, letting users select one or more rows for actions like deletion.

Step 2: Track Selected Rows

Next, we’ll manage the selected rows using the selectionModel state. This state will hold the IDs of the rows that are currently selected. Whenever the selection changes (e.g., when a user checks or unchecks a row), we’ll update the state accordingly.

Step 3: Create a Custom Toolbar

Instead of relying on the default toolbar provided by MUI, we’ll create a custom toolbar that includes a button to trigger the deletion of the selected rows. The button will be enabled only when one or more rows are selected.

Step 4: Delete the Selected Rows

When the user clicks the delete button, we’ll filter out the selected rows from the state and update the table accordingly. This ensures that only the rows that weren’t selected remain in the table.

import React, { useState } from "react";
import { Box, Button, Typography } from "@mui/material";
import { rows as SampleData } from "./data/products";
import EditIcon from "@mui/icons-material/Edit";
import SaveIcon from "@mui/icons-material/Save";
import CancelIcon from "@mui/icons-material/Cancel";
import DeleteIcon from "@mui/icons-material/Delete";
import { toast } from "react-toastify";
import {
  DataGrid,
  GridToolbarContainer,
} from "@mui/x-data-grid";


const CustomToolbar = ({ handleDeleteSelected, selectedRows }) => {
  return (
    <GridToolbarContainer>
      <Button
        variant="contained"
        color="error"
        startIcon={<DeleteIcon />}
        onClick={handleDeleteSelected}
        disabled={selectedRows.length === 0}
      >
        Delete Selected
      </Button>
    </GridToolbarContainer>
  );
};

function DataTable() {
    ......................
const [selectedRows, setSelectedRows] = useState([]);


    const handleDeleteSelected = () => {
        setRows(rows.filter((row) => !selectedRows.includes(row.id)));
        toast.success(`${selectedRows.length} row(s) deleted successfully`);
    };

........

      <DataGrid
        checkboxSelection
        rows={rows}
        columns={columns}
        pageSize={5}
        sx={{
          border: "none",
          width: "100%",
        }}
        getRowId={(row) => row.id}
        paginationModel={paginationModel}
        onPaginationModelChange={(model) => setPaginationModel(model)}
        pageSizeOptions={[5, 10, 15, 20]}
        processRowUpdate={processRowUpdate}
        onProcessRowUpdateError={handleProcessRowUpdateError}
        rowModesModel={rowModesModel}
        slots={{
          toolbar: () => (
            <CustomToolbar
              handleDeleteSelected={handleDeleteSelected}
              selectedRows={selectedRows}
            />
          ),
        }}
        onRowSelectionModelChange={(newSelection) => {
          setSelectedRows(newSelection);
        }}
      />

};

Integrated Implementation of the Data Table with MUI X

In this section, we integrate all the components together into a single file, Table.jsx. The table now handles row selection, updating, and deletion, with toast notifications displayed for each action. Below is the final implementation:

import React, { useState } from "react";
import { Box, Button, Typography } from "@mui/material";
import { rows as SampleData } from "./data/products";
import EditIcon from "@mui/icons-material/Edit";
import SaveIcon from "@mui/icons-material/Save";
import CancelIcon from "@mui/icons-material/Cancel";
import DeleteIcon from "@mui/icons-material/Delete";
import { toast } from "react-toastify";
import {
  DataGrid,
  GridToolbarContainer,
} from "@mui/x-data-grid";


const CustomToolbar = ({ handleDeleteSelected, selectedRows }) => {
  return (
    <GridToolbarContainer>
      <Button
        variant="contained"
        color="error"
        startIcon={<DeleteIcon />}
        onClick={handleDeleteSelected}
        disabled={selectedRows.length === 0}
      >
        Delete Selected
      </Button>
    </GridToolbarContainer>
  );
};

function DataTable() {
  const [paginationModel, setPaginationModel] = useState({
    page: 0,
    pageSize: 5,
  });
  const [rowModesModel, setRowModesModel] = useState({});
  const [rows, setRows] = useState(SampleData);
const [selectedRows, setSelectedRows] = useState([]);

  const sx = {
    display: "flex",
    alignItems: "center",
    height: "100%",
    cursor: "pointer",
  };

    const handleEditClick = (id) => {
        setRowModesModel({ ...rowModesModel, [id]: { mode: "edit" } });
    };

    const handleSaveClick = (id) => {
        setRowModesModel({ ...rowModesModel, [id]: { mode: "view" } });
    };

    const handleCancelClick = (id) => {
        setRowModesModel({
        ...rowModesModel,
        [id]: { mode: "view", ignoreModifications: true },
        });
    };

   const columns = [
     {
       field: "id",
       headerName: "ID",
       flex: 1,
       renderCell: (params) => (
         <Typography sx={sx} variant="body2">
           {params.value}
         </Typography>
       ),
     },
     {
       field: "productName",
       headerName: "Product Name",
       editable: true,
       flex: 3,
       renderCell: (params) => (
         <Typography sx={sx} noWrap={true} variant="body2">
           {params.value}
         </Typography>
       ),
     },
     {
       field: "category",
       headerName: "Category",
       editable: true,
       flex: 1,
       renderCell: (params) => (
         <Typography sx={sx} noWrap={true} variant="body2">
           {params.value}
         </Typography>
       ),
     },
     {
       field: "price",
       headerName: "Price ($)",
       type: "number",
       editable: true,
       flex: 1,
       renderCell: (params) => (
         <Typography sx={sx} noWrap={true} variant="body2">
           {params.value}
         </Typography>
       ),
     },
     {
       field: "stock",
       headerName: "Stock",
       type: "number",
       editable: true,
       flex: 1,
       renderCell: (params) => (
         <Typography sx={sx} noWrap={true} variant="body2">
           {params.value}
         </Typography>
       ),
     },
     {
       field: "rating",
       headerName: "Rating",
       type: "number",
       editable: true,
       flex: 1,
       renderCell: (params) => (
         <Typography sx={sx} noWrap={true} variant="body2">
           {params.value}
         </Typography>
       ),
     },
     {
       field: "supplier",
       headerName: "Supplier",
       editable: true,
       flex: 1,
       renderCell: (params) => (
         <Typography sx={sx} noWrap={true} variant="body2">
           {params.value}
         </Typography>
       ),
     },
     {
       field: "actions",
       type: "actions",
       flex: 1,
       getActions: ({ id }) => {
         const isInEditMode = rowModesModel[id]?.mode === "edit";

         if (isInEditMode) {
           return [
             <SaveIcon
               sx={{ fontSize: "2rem", cursor: "pointer" }}
               onClick={() => handleSaveClick(id)}
             />,
             <CancelIcon
               sx={{ fontSize: "2rem", cursor: "pointer" }}
               onClick={() => handleCancelClick(id)}
             />,
           ];
         }
         return [
           <EditIcon
             sx={{ fontSize: "2rem", cursor: "pointer" }}
             onClick={() => handleEditClick(id)}
           />,
         ];
       },
     },
   ];

     const validateRow = (newRow) => {
       const errors = {};

       if (newRow.price <= 0) {
         errors.price = "Price must be positive";
       }

       if (newRow.rating <= 0) {
         errors.rating = "Rating must be positive";
       }

       if (newRow.stock < 0) {
         errors.stock = "Stock cannot be negative";
       }

       if (newRow.productName.length <= 0) {
         errors.productName = "Product name cannot be empty!";
       }

       return errors;
     };

     const handleProcessRowUpdateError = (error) => {
       const errorDetails = JSON.parse(error.message);
       console.error("Row validation errors:", errorDetails);
       toast.error(
         `Validation Errors: ${Object.values(errorDetails).join(", ")}`
       );
     };

     const processRowUpdate = (newRow) => {
       const errors = validateRow(newRow);
       const updatedRow = { ...newRow, lastUpdated: new Date() };
       if (Object.keys(errors).length > 0) {
         throw new Error(JSON.stringify(errors));
       }
       setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row)));
       toast.success("Row updated successfully!")
       return updatedRow;
     };

    const handleDeleteSelected = () => {
        setRows(rows.filter((row) => !selectedRows.includes(row.id)));
        toast.success(`${selectedRows.length} row(s) deleted successfully`);
    };


  return (
    <Box sx={{ width: "100%" }}>
      <Typography variant="h6" mb={2}>
        Table Content
      </Typography>
      <DataGrid
        checkboxSelection
        rows={rows}
        columns={columns}
        pageSize={5}
        sx={{
          border: "none",
          width: "100%",
        }}
        getRowId={(row) => row.id}
        paginationModel={paginationModel}
        onPaginationModelChange={(model) => setPaginationModel(model)}
        pageSizeOptions={[5, 10, 15, 20]}
        processRowUpdate={processRowUpdate}
        onProcessRowUpdateError={handleProcessRowUpdateError}
        rowModesModel={rowModesModel}
        slots={{
          toolbar: () => (
            <CustomToolbar
              handleDeleteSelected={handleDeleteSelected}
              selectedRows={selectedRows}
            />
          ),
        }}
        onRowSelectionModelChange={(newSelection) => {
          setSelectedRows(newSelection);
        }}
      />
    </Box>
  );
}

export default DataTable;

Now we are done with our implementations. Hurrah 🎊🎊🎊🎈🎈. Here are some snapshots for your reference.

What's Next?

Now that you've successfully implemented the DataGrid with basic functionalities, you can take it to the next level. I recommend exploring the MUI documentation to try out more advanced features and create even more powerful data grids. One such feature is optimizing column visibility, resizing, and persistence, which I've covered in another blog.

You can check it out here:
Optimizing MUI X DataGrid: Column Visibility, Resizing, and Persistence

And that’s a wrap! 🎉 You've successfully learned how to implement a DataGrid and its basic functionalities. Thanks for staying with me till the end.