Optimizing MUI X Data Grid: Column Visibility, Resizing, and Persistence

bchiragbchirag
16 min read

Hey everyone! 😃 Today, we’re diving into MUI X Data Grid. Firstly why MUI X DATA GRID? Because of its ability to handle large volumes of data efficiently, support for sorting, filtering, pagination, and customizable columns. No need to build a custom table. MUI handles all the neccessary operations under the hood.

We will be focusing on two essential features: column visibility and resizing.

In earlier versions of Data Grid, resizing columns was tricky and often required custom code or workarounds. However, with the latest version, 7.22.2, column resizing has become straightforward and much easier to manage.

Another important feature for tables is column visibility. Sometimes, we want certain columns displayed by default, while others stay hidden. MUI X Data Grid makes this easy to configure, and we’ll also cover how to save column visibility and resizing preferences in local storage, so your settings persist across sessions.

Let’s get started!

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

Rehooks/local-storage

Next, we’ll install a library to manage all our local storage functions. This library simplifies handling local storage by providing clean, concise code that looks and feels like standard React code. Behind the scenes, it manages all the necessary operations, allowing us to focus on writing clean, readable code without worrying about implementation details.

Here are the official documentation link. You can also use javascript inbuilt local storage capabilities to store and process the data.

npm i @rehooks/local-storage --save

Here is the Sample data we will using in our code.


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", "releaseDate": "2023-05-01", "sku": "GAMING-LAPTOP-001", "supplierLocation": "USA", "discount": 10, "manufactureDate": "2023-03-15", "lastRestocked": "2023-04-10", "manufacturer": "TechGiant", "warrantyPeriod": "2 years" }
    { "id": 2, "productName": "Latest Model 5G Smartphone with 128GB Storage and AMOLED Display", "category": "Electronics", "price": 899.99, "stock": 30, "rating": 4.6, "supplier": "Future Mobile", "releaseDate": "2023-06-15", "sku": "SMARTPHONE-5G-002", "supplierLocation": "China", "discount": 5, "manufactureDate": "2023-04-01", "lastRestocked": "2023-06-10", "manufacturer": "MobileCorp", "warrantyPeriod": "1 year" }
    { "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", "releaseDate": "2023-12-20", "sku": "HEADPHONES-BT-003", "supplierLocation": "Germany", "discount": 15, "manufactureDate": "2023-02-15", "lastRestocked": "2023-11-01", "manufacturer": "SoundMasters", "warrantyPeriod": "1.5 years" }
    { "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", "releaseDate": "2023-03-10", "sku": "KEYBOARD-RGB-004", "supplierLocation": "USA", "discount": 20, "manufactureDate": "2023-01-10", "lastRestocked": "2023-02-15", "manufacturer": "KeyWorks", "warrantyPeriod": "2 years" }
    { "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", "releaseDate": "2023-07-01", "sku": "MOUSE-WIRELESS-005", "supplierLocation": "South Korea", "discount": 10, "manufactureDate": "2023-04-01", "lastRestocked": "2023-06-25", "manufacturer": "PrecisionTech", "warrantyPeriod": "1 year" }
    { "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", "releaseDate": "2023-02-14", "sku": "FITNESS-TRACKER-006", "supplierLocation": "USA", "discount": 25, "manufactureDate": "2023-01-01", "lastRestocked": "2023-03-01", "manufacturer": "FitLife", "warrantyPeriod": "1 year" }
    { "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", "releaseDate": "2023-04-22", "sku": "TABLET-ANDROID-007", "supplierLocation": "South Korea", "discount": 15, "manufactureDate": "2023-02-15", "lastRestocked": "2023-04-10", "manufacturer": "TabWorks", "warrantyPeriod": "2 years" }
    { "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", "releaseDate": "2022-11-11", "sku": "MONITOR-CURVED-008", "supplierLocation": "USA", "discount": 5, "manufactureDate": "2022-09-01", "lastRestocked": "2023-10-01", "manufacturer": "VisionTech", "warrantyPeriod": "1 year" }
    { "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", "releaseDate": "2023-01-05", "sku": "SPEAKER-BLUETOOTH-009", "supplierLocation": "China", "discount": 30, "manufactureDate": "2022-12-01", "lastRestocked": "2023-11-01", "manufacturer": "BlissSound", "warrantyPeriod": "1 year" }
    { "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", "releaseDate": "2023-05-25", "sku": "HDD-EXTERNAL-010", "supplierLocation": "Germany", "discount": 10, "manufactureDate": "2023-04-05", "lastRestocked": "2023-05-20", "manufacturer": "StorageMasters", "warrantyPeriod": "2 years" }
];

App.jsx

In this section, we will implement the base code for displaying the table. This basic code can be added directly to the App.js file, but it can also be reused as a component anywhere within your app. The App.js file will primarily contain the table component and the MUI theme, which we have already set up.

import { Box, Typography } from "@mui/material";
import Table from "./Table";
import { ThemeProvider } from "./ThemeContext";

function App() {

  return (
    <ThemeProvider>
      <Box
        sx={{
          display: "flex", 
          flexDirection: "column", 
          maxWidth: "100vw",
          maxHeight: "100vh", 
          overflow: "auto", 
          pr: 6,
          pl: 6,
          py: 6,
        }}
      >
        <Table />
      </Box>
    </ThemeProvider>
  );
}

export default App;

Table.jsx

In this step, we will begin by creating our table component. First, we define the data in an external file and import it into the table file. The initial focus will be on rendering the entire dataset within the table. Once that is in place, we'll move on to adding features like column visibility and resizing.

export const getRegularDate = (datetime) => {
  if (!datetime) return "";

  const date = new Date(datetime);

  const month = String(date.getMonth() + 1).padStart(2, "0"); // getMonth() returns 0-11
  const day = String(date.getDate()).padStart(2, "0");
  const year = date.getFullYear();

  return `${month}/${day}/${year}`;
};
import React, { useEffect } from "react";
import { DataGrid } from "@mui/x-data-grid";
import { Box, Typography, useTheme } from "@mui/material";
import { rows } from "./data/products";
import { getRegularDate } from "./utils/date.util";

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

const columns = [
  { field: "id", headerName: "ID", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value}</Typography> },
  { field: "productName", headerName: "Product Name", flex: 3, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value}</Typography> },
  { field: "category", headerName: "Category", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value}</Typography> },
  { field: "price", headerName: "Price ($)", type: "number", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value}</Typography> },
  { field: "stock", headerName: "Stock", type: "number", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value}</Typography> },
  { field: "rating", headerName: "Rating", type: "number", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value}</Typography> },
  { field: "supplier", headerName: "Supplier", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value}</Typography> },
  { field: "releaseDate", headerName: "Release Date", type: "date", flex: 1, valueGetter: (value) => value ? new Date(value) : null, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value ? getRegularDate(params.value) : "-"}</Typography> },
  { field: "sku", headerName: "SKU", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value}</Typography> },
  { field: "supplierLocation", headerName: "Supplier Location", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value}</Typography> },
  { field: "discount", headerName: "Discount (%)", type: "number", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value}</Typography> },
  { field: "manufactureDate", headerName: "Manufacture Date", type: "date", flex: 1, valueGetter: (value) => value ? new Date(value) : null, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value ? getRegularDate(params.value) : "-"}</Typography> },
  { field: "lastRestocked", headerName: "Last Restocked", type: "date", flex: 1, valueGetter: (value) => value ? new Date(value) : null, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value ? getRegularDate(params.value) : "-"}</Typography> },
  { field: "manufacturer", headerName: "Manufacturer", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value}</Typography> },
  { field: "warrantyPeriod", headerName: "Warranty Period", flex: 1, renderCell: (params) => <Typography sx={sx} variant="body3">{params.value}</Typography> }
];

 return (
      <Box sx={{ width: "100%", overflow: "hidden" }}>
        <Typography variant="h6" mb={2}>
          Table Content
        </Typography>
        <DataGrid
          disableColumnMenu={false}
          rows={rows}
          columns={columns}
          pageSize={5}
          sx={{
            border: "none",
            overflowY: "hidden",
            width: "100%",
          }}
          getRowId={(row) => row.id}
        />
      </Box>
    );
};

Now the basic table should be visible. As always we can add more styling to make it look more perfect. Well that is a topic for another day.

Let’s start with the column visibility. Firstly we will need a default model to define the column visibility lets add that.

    const defaultVisibilityModel = {
    id: true,
    productName: true,
    category: true,
    price: true,
    stock: true,
    rating: true,
    supplier: false,
    releaseDate: false,
    sku: false,
    supplierLocation: false,
    discount: false,
    manufactureDate: false,
    lastRestocked: false,
    manufacturer: false,
    warrantyPeriod: false,
    };

Now we will use rehooks library to interact with localStorage to save these states. Now let’s import them.

import { writeStorage, useLocalStorage } from "@rehooks/local-storage";

After defining the default column configuration, we’ll need a way to store and retrieve these settings in localStorage, allowing us to maintain the user’s column preferences across sessions. For this, we’ll use the useLocalStorage function from the rehooks library, which enables us to easily get the latest state and update it in a way similar to useState .

  const [visibilityModel, setVisibilityModel] = useLocalStorage(
    `columnvisibility`,
    defaultVisibilityModel
    );

We will also need a function to handle visibility changes, allowing users to customize which columns are displayed. This function will dynamically update our column visibility settings, ensuring a more personalized and persistent table view.

 const handleVisibilityChange = (newVisibilityModel) => {
    const updatedVisibility = {
        ...visibilityModel,
        ...newVisibilityModel,
    };

    setVisibilityModel(updatedVisibility);
    writeStorage(`columnvisibility`, updatedVisibility);
    };

To ensure persistence, we’ll use useEffect to retrieve the initial state from localStorage on the initial render. If no saved state is found, the default model is used. However, if a saved configuration exists, it updates the component’s state accordingly. This approach guarantees that user preferences are maintained across sessions.

    useEffect(() => {
    if (!visibilityModel || Object.keys(visibilityModel).length === 0) {
        setVisibilityModel(defaultVisibilityModel);
        writeStorage(
        `columnvisibility`,
        defaultVisibilityModel
        );
        apiRef.current.setColumnVisibilityModel(defaultVisibilityModel);
    } else {
        apiRef.current.setColumnVisibilityModel(visibilityModel);
    }
    }, []);

Lastly we will need to add to map these changes with datagrid. We will map the handleVisibilityChange with datagrid this how the datagrid will look.

  <DataGrid
          apiRef={apiRef}
          disableColumnMenu={false}
          rows={rows}
          columns={columns}
          pageSize={5}
          getRowId={(row) => row.id}
          columnVisibilityModel={visibilityModel}
          onColumnVisibilityModelChange={handleVisibilityChange}
        />

Lastly, we need to connect these visibility settings to the DataGrid so that changes in visibility are reflected directly in the table. By mapping the handleVisibilityChange function to the DataGrid, we ensure that any updates to column visibility are immediately applied to the table display.

We can also add a column selector icon at the top of the DataGrid to allow users to show or hide columns. Clicking this icon opens a popup listing all columns, where users can select or deselect columns based on their preferences. Below is the snippet for creating a ColumnSection component that renders this column selection popup:

ColumnSection.jsx

import React, { useState } from "react";
import {
  Box,
  Stack,
  IconButton,
  Typography,
  Menu,
  MenuItem,
  FormControlLabel,
  Checkbox,
} from "@mui/material";
import ViewWeekIcon from "@mui/icons-material/ViewWeek";
import { writeStorage } from "@rehooks/local-storage";

const ColumnsSection = ({
  visibilityModel,
  setVisibilityModel,
  columns,
  label,
}) => {
  const [anchorEl, setAnchorEl] = useState(null); 
  const open = Boolean(anchorEl);

  const handleOpenManageColumns = (event) => {
    setAnchorEl(event.currentTarget); 
  };

  const handleCloseManageColumns = () => {
    setAnchorEl(null);
  };

  const handleToggleColumnVisibility = (event, field) => {
    const newVisibilityModel = {
      ...visibilityModel,
      [field]: event.target.checked,
    };
    setVisibilityModel(newVisibilityModel);
    writeStorage(label, newVisibilityModel);
  };

  return (
    <Box sx={{ display: "flex", mb: 2 }}>
      <Stack
        onClick={handleOpenManageColumns}
        direction="row"
        gap={0.5}
        alignItems="center"
      >
        <IconButton>
          <ViewWeekIcon sx={{ fontSize: "2rem" }} />
        </IconButton>
        <Typography variant="body2" sx={{ cursor: "pointer" }}>
          COLUMNS
        </Typography>
      </Stack>

      <Menu
        anchorEl={anchorEl}
        open={open}
        onClose={handleCloseManageColumns}
        anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
        transformOrigin={{ vertical: "top", horizontal: "left" }}
        sx={{
          maxWidth: "100%",
          "& .MuiMenu-paper": { width: "max-content", maxWidth: "90vw", maxHeight: "50vh" },
        }}
      >
        {Object.keys(visibilityModel).map((field) => {
          const column = columns.find((col) => col.field === field);
          return (
            <MenuItem
              key={field}
              sx={{
                paddingY: 0.5,
                minHeight: "32px",
                maxWidth: "90vw", 
                whiteSpace: "nowrap", 
                overflow: "hidden",
                textOverflow: "ellipsis",
              }}
              title={column?.headerName} 
            >
              <FormControlLabel
                control={
                  <Checkbox
                    checked={visibilityModel[field] ?? true}
                    onChange={(event) =>
                      handleToggleColumnVisibility(event, field)
                    }
                  />
                }
                label={
                  <Typography variant="body2" noWrap>
                    {column?.headerName}
                  </Typography>
                }
              />
            </MenuItem>
          );
        })}
      </Menu>
    </Box>
  );
};

export default ColumnsSection;

Adding this final snippet integrates everything together, providing users a visual interface to show or hide table columns dynamically.

<ColumnsSection
   visibilityModel={visibilityModel}
   setVisibilityModel={setVisibilityModel}
   columns={columns}
   label={`columnvisibility`}
/>

This is how it should look like.

Persisting Column Widths

Now that we've covered column visibility persistence, let's move on to persisting column widths.

Similar to how we handled the visibility model, we need to set up a state to track the column widths, update them as necessary, and save them to localStorage. This will allow the column widths to persist even after a page refresh.

To achieve this, we will:

  1. Use the useLocalStorage hook to store the column widths in localStorage.

  2. Implement a function to handle column resizing and update the state accordingly.

  3. Use the useEffect hook to apply the saved column widths when the component loads.

Here’s the code that sets up column width persistence:

const [columnWidths, setColumnWidths] = useLocalStorage('columnWidths', {});

const handleColumnResize = (params) => {
    const updatedWidths = {
        ...columnWidths,
        [params.colDef.field]: params.width,
    };
    setColumnWidths(updatedWidths);
    writeStorage('columnWidths', updatedWidths);
};

useEffect(() => {
    const savedWidths = columnWidths;

    if (savedWidths) {
        Object.entries(savedWidths).forEach(([field, width]) => {
            apiRef.current.setColumnWidth(field, width);
        });
    }
}, [apiRef, columnWidths]);

Explanation:

  • State Setup: We initialize the column widths state using useLocalStorage, which stores the column widths persistently.

  • Column Resize Handler: The handleColumnResize function is triggered when a column is resized. It updates the state and stores the new width in localStorage.

  • Effect Hook: The useEffect hook checks for any previously saved column widths in localStorage. If they exist, it applies them to the respective columns when the component renders.

Finally, to use these persisted column widths, you’ll need to dynamically update the column definitions in the DataGrid.

By implementing this, your column widths will remain consistent across sessions, even after the page is refreshed.

To use the column widths we will have to update the columns to persist this width.

const columns = [
{
  field: "id", headerName: "ID", flex: columnWidths["id"] ? 0 : 1, width: columnWidths["id"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
},
{
  field: "productName", headerName: "Product Name", flex: columnWidths["productName"] ? 0 : 3, width: columnWidths["productName"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
},
{
  field: "category", headerName: "Category", flex: columnWidths["category"] ? 0 : 1, width: columnWidths["category"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
},
{
  field: "price", headerName: "Price ($)", type: "number", flex: columnWidths["price"] ? 0 : 1, width: columnWidths["price"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
},
{
  field: "stock", headerName: "Stock", type: "number", flex: columnWidths["stock"] ? 0 : 1, width: columnWidths["stock"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
},
{
  field: "rating", headerName: "Rating", type: "number", flex: columnWidths["rating"] ? 0 : 1, width: columnWidths["rating"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
},
{
  field: "supplier", headerName: "Supplier", flex: columnWidths["supplier"] ? 0 : 1, width: columnWidths["supplier"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
},
{
  field: "releaseDate", headerName: "Release Date", type: "date", flex: columnWidths["releaseDate"] ? 0 : 1, width: columnWidths["releaseDate"], valueGetter: (value) => value ? new Date(value) : null, renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value ? getRegularDate(params.value) : "-"}</Typography>,
},
{
  field: "sku", headerName: "SKU", flex: columnWidths["sku"] ? 0 : 1, width: columnWidths["sku"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
},
{
  field: "supplierLocation", headerName: "Supplier Location", flex: columnWidths["supplierLocation"] ? 0 : 1, width: columnWidths["supplierLocation"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
},
{
  field: "discount", headerName: "Discount (%)", type: "number", flex: columnWidths["discount"] ? 0 : 1, width: columnWidths["discount"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
},
{
  field: "manufactureDate", headerName: "Manufacture Date", type: "date", flex: columnWidths["manufactureDate"] ? 0 : 1, width: columnWidths["manufactureDate"], valueGetter: (value) => value ? new Date(value) : null, renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value ? getRegularDate(params.value) : "-"}</Typography>,
},
{
  field: "lastRestocked", headerName: "Last Restocked", type: "date", flex: columnWidths["lastRestocked"] ? 0 : 1, width: columnWidths["lastRestocked"], valueGetter: (value) => value ? new Date(value) : null, renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value ? getRegularDate(params.value) : "-"}</Typography>,
},
{
  field: "manufacturer", headerName: "Manufacturer", flex: columnWidths["manufacturer"] ? 0 : 1, width: columnWidths["manufacturer"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
},
{
  field: "warrantyPeriod", headerName: "Warranty Period", flex: columnWidths["warrantyPeriod"] ? 0 : 1, width: columnWidths["warrantyPeriod"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
},
];

Here’s how the final data grid will look like.

        <DataGrid
          apiRef={apiRef}
          disableColumnMenu={false}
          rows={rows}
          columns={columns}
          pageSize={5}
          sx={{
            border: "none",
            overflowY: "hidden",
            width: "100%",
          }}
          getRowId={(row) => row.id}
          onColumnResize={handleColumnResize}
          columnVisibilityModel={visibilityModel}
          onColumnVisibilityModelChange={handleVisibilityChange}
        />

Final Code:

import React, { useEffect } from "react";
import { DataGrid, useGridApiRef } from "@mui/x-data-grid";
import { Box, Typography, useTheme } from "@mui/material";
import { rows } from "./data/products";
import { getRegularDate } from "./utils/date.util";
import { writeStorage, useLocalStorage } from "@rehooks/local-storage";
import ColumnsSection from "./ColumnSection";

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

function DataTable() {
    const theme = useTheme();  
    const apiRef = useGridApiRef();

    const defaultVisibilityModel = {
    id: true,
    productName: true,
    category: true,
    price: true,
    stock: true,
    rating: true,
    supplier: false,
    releaseDate: false,
    sku: false,
    supplierLocation: false,
    discount: false,
    manufactureDate: false,
    lastRestocked: false,
    manufacturer: false,
    warrantyPeriod: false,
    };

    const [visibilityModel, setVisibilityModel] = useLocalStorage(
    `columnvisibility`,
    defaultVisibilityModel
    );

    const [columnWidths, setColumnWidths] = useLocalStorage(
    `columnWidths`,
    {}
    );

const columns = [
    {
      field: "id", headerName: "ID", flex: columnWidths["id"] ? 0 : 1, width: columnWidths["id"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
    },
    {
      field: "productName", headerName: "Product Name", flex: columnWidths["productName"] ? 0 : 3, width: columnWidths["productName"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
    },
    {
      field: "category", headerName: "Category", flex: columnWidths["category"] ? 0 : 1, width: columnWidths["category"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
    },
    {
      field: "price", headerName: "Price ($)", type: "number", flex: columnWidths["price"] ? 0 : 1, width: columnWidths["price"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
    },
    {
      field: "stock", headerName: "Stock", type: "number", flex: columnWidths["stock"] ? 0 : 1, width: columnWidths["stock"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
    },
    {
      field: "rating", headerName: "Rating", type: "number", flex: columnWidths["rating"] ? 0 : 1, width: columnWidths["rating"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
    },
    {
      field: "supplier", headerName: "Supplier", flex: columnWidths["supplier"] ? 0 : 1, width: columnWidths["supplier"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
    },
    {
      field: "releaseDate", headerName: "Release Date", type: "date", flex: columnWidths["releaseDate"] ? 0 : 1, width: columnWidths["releaseDate"], valueGetter: (value) => value ? new Date(value) : null, renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value ? getRegularDate(params.value) : "-"}</Typography>,
    },
    {
      field: "sku", headerName: "SKU", flex: columnWidths["sku"] ? 0 : 1, width: columnWidths["sku"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
    },
    {
      field: "supplierLocation", headerName: "Supplier Location", flex: columnWidths["supplierLocation"] ? 0 : 1, width: columnWidths["supplierLocation"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
    },
    {
      field: "discount", headerName: "Discount (%)", type: "number", flex: columnWidths["discount"] ? 0 : 1, width: columnWidths["discount"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
    },
    {
      field: "manufactureDate", headerName: "Manufacture Date", type: "date", flex: columnWidths["manufactureDate"] ? 0 : 1, width: columnWidths["manufactureDate"], valueGetter: (value) => value ? new Date(value) : null, renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value ? getRegularDate(params.value) : "-"}</Typography>,
    },
    {
      field: "lastRestocked", headerName: "Last Restocked", type: "date", flex: columnWidths["lastRestocked"] ? 0 : 1, width: columnWidths["lastRestocked"], valueGetter: (value) => value ? new Date(value) : null, renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value ? getRegularDate(params.value) : "-"}</Typography>,
    },
    {
      field: "manufacturer", headerName: "Manufacturer", flex: columnWidths["manufacturer"] ? 0 : 1, width: columnWidths["manufacturer"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
    },
    {
      field: "warrantyPeriod", headerName: "Warranty Period", flex: columnWidths["warrantyPeriod"] ? 0 : 1, width: columnWidths["warrantyPeriod"], renderCell: (params) => <Typography sx={{ ...sx, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} variant="body3">{params.value}</Typography>,
    },
];

    const handleVisibilityChange = (newVisibilityModel) => {
    const updatedVisibility = {
        ...visibilityModel,
        ...newVisibilityModel,
    };

    setVisibilityModel(updatedVisibility);
    writeStorage(`columnvisibility`, updatedVisibility);
    };

    useEffect(() => {
    const savedWidths = columnWidths;

    if (savedWidths) {
        Object.entries(savedWidths).forEach(([field, width]) => {
        apiRef.current.setColumnWidth(field, width);
        });
    }
    }, [apiRef, columnWidths]);

    useEffect(() => {
    if (!visibilityModel || Object.keys(visibilityModel).length === 0) {
        setVisibilityModel(defaultVisibilityModel);
        writeStorage(
        `columnvisibility`,
        defaultVisibilityModel
        );
        apiRef.current.setColumnVisibilityModel(defaultVisibilityModel);
    } else {
        apiRef.current.setColumnVisibilityModel(visibilityModel);
    }
    }, []);

    return (
      <Box sx={{ width: "100%", overflow: "hidden" }}>
        <Typography variant="h6" mb={2}>
          Table Content
        </Typography>
        <ColumnsSection
          visibilityModel={visibilityModel}
          setVisibilityModel={setVisibilityModel}
          columns={columns}
          label={`columnvisibility`}
        />
        <DataGrid
          apiRef={apiRef}
          disableColumnMenu={false}
          rows={rows}
          columns={columns}
          pageSize={5}
          sx={{
            border: "none",
            overflowY: "hidden",
            width: "100%",
          }}
          getRowId={(row) => row.id}
          onColumnResize={handleColumnResize}
          columnVisibilityModel={visibilityModel}
          onColumnVisibilityModelChange={handleVisibilityChange}
        />
      </Box>
    );
}

export default DataTable;

And that's it! 🎉 We've successfully added persistence for both column visibility and column widths. Now, even after refreshing, the state will be saved—your customizations stay intact!

With the useLocalStorage hook, we ensure that every time you interact with the DataGrid, whether adjusting column widths or hiding/showing columns, these preferences are stored and applied seamlessly.

Congratulations! You’ve now built a fully persistent DataGrid with Material-UI. 🎉

2
Subscribe to my newsletter

Read articles from bchirag directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

bchirag
bchirag