Caleb Koch
Caleb Koch

Reputation: 874

How do you prevent a re-created React component from losing state?

The example in this sandbox is a contrived example, but it illustrates the point.

Clicking the "Add column" button is supposed to add a new column. It works the first time, but doesn't work after that. You'll notice from the log that this issue has to do with the fact that columns is always in its original state. Therefore, a column is always being added to this original state, not the current state.

I imagine this issue is related to the fact that the column header is being re-created on each call to renderHeader, but I'm unsure about how to pass the state to the newly created header component.

Upvotes: 1

Views: 143

Answers (2)

Soufiane Boutahlil
Soufiane Boutahlil

Reputation: 2604

There are 2 ways to update your state:

  • The first way: setColumns(newColumns)
  • The second way: setColumns(columns => newColumns)

You should use the second way because the new state depend on the current state.

onClick={() => {
  setColumns((columns) => {
    console.log(columns.map((column) => column.field));
    const newColumnName = `Column ${columns.length}`;
    const newColumns = [...columns];
    newColumns.splice(
      columns.length - 1,
      0,
      createColumn(newColumnName)
    );
    return newColumns;
  });
}}

Your component:

import React, { useState } from "react";
import { DataGrid } from "@mui/x-data-grid";
import Button from "@mui/material/Button";
import "./styles.css";

export default function App() {
  const [rows] = useState([
    { id: 1, "Column 1": 1, "Column 2": 2 },
    { id: 2, "Column 1": 3, "Column 2": 3 },
    { id: 3, "Column 1": 4, "Column 2": 5 }
  ]);
  const createColumn = (name) => {
    return {
      field: name,
      align: "center",
      editable: true,
      sortable: false
    };
  };
  const [columns, setColumns] = useState([
    createColumn("Column 1"),
    createColumn("Column 2"),
    {
      field: "Add a split",
      width: 150,
      sortable: false,
      renderHeader: (params) => {
        return (
          <Button
            variant="contained"
            onClick={() => {
              setColumns((columns) => { // <== use callback
                console.log(columns.map((column) => column.field));
                const newColumnName = `Column ${columns.length}`;
                const newColumns = [...columns];
                newColumns.splice(
                  columns.length - 1,
                  0,
                  createColumn(newColumnName)
                );
                return newColumns;
              });
            }}
          >
            Add column
          </Button>
        );
      }
    }
  ]);

  return (
    <div className="App">
      <DataGrid
        className="App-data-grid"
        rows={rows}
        columns={columns}
        disableSelectionOnClick
        disableColumnMenu
      />
    </div>
  );
}

https://codesandbox.io/s/mui-sandbox-forked-1im0p?file=/src/App.js:0-1562

Upvotes: 2

Code-Apprentice
Code-Apprentice

Reputation: 83517

I think this has to do with the way the closure for (params) => {...} captures the value of columns. At the same time, you have this strange circular thing going on: useState() returns columns and also uses columns in one of its parameters. You will need to find a way to break this circularity.

Upvotes: 0

Related Questions