Table of contents
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:
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.
- To make a column editable, include
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.
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:
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.
- The
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.
- In case any validation fails during the row update, we can handle errors using
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.