Search code examples
javascriptreactjsmaterial-uimui-x-data-grid

How can I prevent constant re-rendering of an MUI `DataGrid` on row commit?


Here is how the MUI documentation did it:

const rows = [/* Row Data */]
<DataGrid
rows={rows}
{/* Other Props */} /> /*[1]*/

I admit that this is an excellent way to do it. It is very efficient and DataGrid now handles state on its own.

But it doesn’t work for my use case because rows is dynamic and comes in over the wire. So this is how I did it:

const [rows, set_rows] = useState([])

/* Within some subroutine */
——begin
    let data = await /* A network request that gets the rows */
    set_rows(data)
——end

This works and renders the data fine but the problem now is that with each row commit, I must call set_rows() to synchronize changes with the GUI. Now guess what happens each time? You guessed it, a GUI re-render.

How do I prevent this re-render?


Permit me to reiterate. In the MUI DataGrid documentation, rows are defined const and passed as input to DataGrid.props.rows making state updates efficient and handled automatically by DataGrid.

In my case, I cannot declare it const and hardcode it because the data is dynamic. I could technically declare it let and mutate the variable after network response but after the DataGrid renders, mutations are no longer detectable by the GUI. Hence I must use useState() so that changes will be reflected in the GUI. If I use useState(), I will have to manage state updates on my own end and the DataGrid re-renders constantly with each state update.


[1] The sample was stripped down excessively to get straight to the point.


Solution

  • If I understood your issue correctly, one solution could be to separate the initial data (rows) from the modified data (modifiedRowsRef).

    To prevent constant re-rendering of an MUI DataGrid on row commit while still accessing modified rows, you can try this:

    • Initial Data: Fetch from an API and store in the rows state.
    • Modified Data: Stored in a ref (modifiedRowsRef) to avoid causing re-renders when updated.

    import React, { useEffect, useState, useCallback, useRef } from "react";
    import { DataGrid, GridRowModel, GridColDef, GridValidRowModel } from "@mui/x-data-grid";
    
    interface RowData extends GridValidRowModel {
      id: number;
      lastName: string;
      firstName: string;
      age: number | null;
    }
    
    const columns: GridColDef[] = [
      { field: "id", headerName: "ID", width: 70 },
      { field: "firstName", headerName: "First name", width: 130, editable: true },
      { field: "lastName", headerName: "Last name", width: 130, editable: true },
      { field: "age", headerName: "Age", type: 'number', width: 90, editable: true },
    ];
    
    const MyDataGrid: React.FC = () => {
      const [rows, setRows] = useState<RowData[]>([]);
      const modifiedRowsRef = useRef<{ [id: number]: RowData }>({});
    
    
      // Check if component is rendered
      useEffect(() => {
        console.log("MyDataGrid component rendered");
      });
    
      useEffect(() => {
        // Simulate an API call
        setTimeout(() => {
          setRows([
            { id: 1, lastName: "Snow", firstName: "Jon", age: 35 },
            { id: 2, lastName: "Lannister", firstName: "Cersei", age: 42 },
            { id: 3, lastName: "Lannister", firstName: "Jaime", age: 45 },
            { id: 4, lastName: "Stark", firstName: "Arya", age: 16 },
            { id: 5, lastName: "Targaryen", firstName: "Daenerys", age: null },
          ]);
        }, 2000);
      }, []);
    
      // Function to get the entire data structure
      const getEntireDataStructure = () => {
        const mergedData = rows.map(row => modifiedRowsRef.current[row.id] || row);
        console.log("Entire Data Structure:", mergedData);
        return mergedData;
      };
    
      // Function to send the entire data structure to the server
      const updateDataOnServer = useCallback(() => {
        const updatedData = getEntireDataStructure();
        console.log("Updated Data:", updatedData);
        // Send updatedData to server
      }, [rows]);
    
      // Process row update
      const processRowUpdate = useCallback(
        (newRow: GridRowModel, oldRow: GridRowModel): GridRowModel => {
          const updatedRow = newRow as RowData;
    
          modifiedRowsRef.current = {
            ...modifiedRowsRef.current,
            [updatedRow.id]: updatedRow,
          };
    
          // Log the current state of modifiedRowsRef
          console.log("Modified Rows:", modifiedRowsRef.current);
    
          // Update data on the server
          updateDataOnServer();
    
          return updatedRow;
        },
        [updateDataOnServer]
      );
    
      return (
        <div style={{ height: 400, width: "100%" }}>
          <DataGrid
            rows={rows}
            columns={columns}
            initialState={{
              pagination: {
                paginationModel: { pageSize: 5, page: 0 },
              },
            }}
            pageSizeOptions={[5]}
            checkboxSelection
            processRowUpdate={processRowUpdate}
            onProcessRowUpdateError={(error) => console.error("Update error:", error)}
            loading={rows.length === 0}
          />
          <button onClick={getEntireDataStructure}>
            Get Entire Data Structure
          </button>
        </div>
      );
    };
    
    export default React.memo(MyDataGrid);

    I assume you would like to update the server-side data on commit, and therefore I added these functions

    • getEntireDataStructure: Merges the initial data with the modified data to get the complete data structure.
    • updateDataOnServer: Send the entire data structure to the server.

    Conclusion, any state update in ReactNode will trigger a GUI re-render