mui / mui-x

MUI X: Build complex and data-rich applications using a growing list of advanced React components, like the Data Grid, Date and Time Pickers, Charts, and more!
https://mui.com/x/
4.52k stars 1.31k forks source link

[data grid] Loss of table states on re-renders #14612

Closed carlosg-mscope closed 1 month ago

carlosg-mscope commented 1 month ago

Steps to reproduce

Steps:

  1. Make any changes on the table. Like filtering, reordering, and change column widths.
  2. Make a dispatch or any state change on any handler, that triggers any parent re render.
  3. The table losses the current state (filters, ordering, widths, etc) and revert to the initial state.

Current behavior

As mention, the state is lost whenever we make any changes on the parents components where the table lives.

Expected behavior

To keep the state by default. If there is no controlled table inside models.

Context

We have already try to make controlled states, with the models props, and persist them (at the localstorage for convenience) as the documentation recommends. But still some states are loss, and the Grid is erratic . For example whenever we filter by the operator "contains", if you use a handler that in the parent updates some state, then the table loss it state inmediatly and close the filter modal, even if you are still typing.

Also we are already using memo for the columns and rows props, and useCallbacks for the handlers so that they dont change reference. But still it does not work, even if we leave the dependecy array empty so it computes only once. But I think that the component should expect changes in column definition, since some applications might have complex custom components inside the renderers, that could need some state from the parent.

Also I think this has to do with some other recently issues

Here are some important parts of the code definitions:


//Columns definition:
const columns = useMemo(() => [
  {
    field: "sectorId",
    filterable: false,
  },
  {
    field: "swTop",
    filterable: false,
  },
  {
    field: "id_plataforma",
    filterable: false,
  },
  {
    field: "name",
    headerName: dictionary["companies"],
    width: smallScreen ? 180 : 360,
    type: "string",
    groupable: false,
    renderHeader: (params) => {
      return (
        <BaseBox
          sx={{
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
          }}
        >
          <BaseTypography
            sx={{
              ...theme.typography.mid.s.regular,
              color: theme.palette.light.content.mid,
            }}
          >
            {`${params.colDef.headerName} ${companiesCount?.length}`}
          </BaseTypography>
        </BaseBox>
      );
    },
    renderCell: (params) => {
      return (
        <BaseLink
          target="_blank"
          href={`/company/${params.row.swTop}/${params.row.id_plataforma}`}
          sx={{
            cursor: "pointer",
            ...theme.typography.mid.s.regular,
            color: theme.palette.light.content.high,
            textDecoration: "none",
            "&:hover": {
              color: theme.palette.light.content.high,
              textDecoration: "underline",
              textDecorationColor: theme.palette.light.content.low,
            },
          }}
          onClick={(e) => {
            mixpanel.track(MXP_TABLE_VIEW_COPANY_DETAIL, {
              Module: "Intelligence",
              Client: clientName,
              companyPlatformId: params.row.id_plataforma,
            });
          }}
        >
          <BaseBox
            sx={{
              overflow: "hidden",
              textOverflow: "ellipsis",
              whiteSpace: "nowrap",
              width: params.colDef.computedWidth - 20,
            }}
          >
            {params.value?.toUpperCase()}
          </BaseBox>
        </BaseLink>
      );
    },
  },
  {
    field: "province",
    headerName: dictionary["province"],
    type: "singleSelect",
    valueOptions: mappedRegions,
    width: smallScreen ? 120 : 200,
    groupable: false,
    renderCell: (params) => {
      return (
        <BaseBox
          sx={{
            overflow: "hidden",
            textOverflow: "ellipsis",
            whiteSpace: "nowrap",
            width: params.colDef.computedWidth - 20,
          }}
        >
          {params.value}
        </BaseBox>
      );
    },
  },
  {
    field: "fiscalPeriod",
    headerName: dictionary["year"],
    width: smallScreen ? 50 : 100,
    type: "number",
    headerAlign: "left",
    align: "left",
    groupable: false,
  },
  {
    field: "nombreTendencia3",
    headerName: dictionary["microsector"],
    width: smallScreen ? 260 : 360,
    type: "singleSelect",
    valueOptions: mappedMicrosectors,
    groupable: false,
    renderCell: (params) => {
      const ratingLetter = params?.row.rating ? params.row.rating[0] : "";
      const sectorCategoryInfo = sectorCategories.find(
        (e) => e.sectorCategory === ratingLetter
      );
      let rotation =
        ratingLetter === "B"
          ? 90
          : ratingLetter === "C"
          ? 135
          : ratingLetter === "C"
          ? 180
          : 0;
      const cellComponent = params.value ? (
        <BaseBox
          sx={{
            display: "flex",
            alignItems: "center",
            justifyContent: "flex-start",
            height: "100%",
            width: "90%",
            maxWidth: "fit-content",
            marginLeft: "0px",
          }}
        >
          {linkEnable ? (
            <Link
              style={{ width: "100%" }}
              target="_blank"
              to={"microsegmentation/" + params.row.sectorId}
              onClick={() => {
                dispatch({
                  type: types.SEND_MIXPANEL_EVENT,
                  nameEvent: MXP_VIEW_MICROSECTOR_DETAIL,
                  propertiesEvent: {
                    ID: params.row.id_plataforma
                  },
                });
              }}
            >
              {HighlightTextWrapper({
                sectorCategoryInfo,
                sectorCategoryLetter: ratingLetter,
                description: params.value,
                rotation,
              })}
            </Link>
          ) : (
            HighlightTextWrapper({
              sectorCategoryInfo,
              sectorCategoryLetter: ratingLetter,
              description: params.value,
              rotation,
            })
          )}
        </BaseBox>
      ) : (
        <BaseTypography
          sx={{
            color: theme.palette.light.content.low,
            ...theme.typography.mid.s.regular,
          }}
        >
          {"N/A"}
        </BaseTypography>
      );
      return cellComponent;
    },
  },
  {
    field: "swSeed",
    headerName: dictionary["classifAccuracy"],
    width: 120,
    type: "singleSelect",
    valueOptions: [dictionary["verified"], dictionary["suggested"]],
    groupable: false,
    renderCell: (params) => {
      return params.value ? (
        <BaseBox
          sx={{
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            height: "26px",
            width: "fit-content",
            marginLeft: "0px",
            backgroundColor: theme.palette.tag[1],
            borderRadius: "4px",
          }}
        >
          <BaseTypography
            sx={{
              ...theme.typography.mid.s.regular,
              color: theme.palette.light.content.high,
              textAlign: "center",
              padding: "0 6px",
            }}
          >
            {params.value}
          </BaseTypography>
        </BaseBox>
      ) : null;
    },
  },
  {
    field: "rating",
    headerName: dictionary["score"],
    width: 58,
    maxWidth: 100,
    type: "singleSelect",
    valueOptions: mappedRatings,
    groupable: false,
    cellClassName: (params) => {
      if (params.value?.startsWith("A")) return "rating-a";
      if (params.value?.startsWith("B")) return "rating-b";
      if (params.value?.startsWith("C")) return "rating-c";
      return "";
    },
  },
  {
    field: "sales",
    headerName: `${setMonetaryUnit(dictionary["sales"], currencySymbol)}`,
    flex: 1,
    type: "number",
    valueFormatter: negativeValueFormatter,
    groupable: false,
  },
  {
    field: "cagr5YearsSales",
    headerName: `${setMonetaryUnit(dictionary["cagr5TO"], currencySymbol)}`,
    flex: 1,
    type: "number",
    valueFormatter: negativeValueFormatter,
    groupable: false,
  },
], []);

//Rows definition:
const mappedArray = useMemo(() => {
  return classifications.map((item) => ({
    id: item.cif,
    swTop: item.swTop,
    id_plataforma: item.code,
    name: item.name,
    province: item.province,
    fiscalPeriod: item.lastFiscalPeriod,
    swSeed: item.swSeed === 1 ? dictionary["verified"] : dictionary["suggested"],
    nombreTendencia3: item.NOMBRE_TENDENCIA3,
    sectorId: item.sectorId,
    rating: item.sectorCategory && item.companyType
      ? `${item.sectorCategory}${item.companyType}`
      : item.companyType 
      ? `${item.companyType}`
      : "",
    sales: item.sales,
    cagr5YearsSales: item.cagr5YearsSales,
    ebitda: item.ebitda,
    cagr5YearsEbitda: item.cagr5YearsEbitda,
    debt3A: item.debt3A,
    marginBai3A: item.marginBai3A,
    marginEbitda3A: item.marginEbitda3A,
    roa3A: item.roa3A,
    roe3A: item.roe3A,
    solvency3A: item.solvency3A,
    cirbeCuote: item.cirbeCuote,
    grossMarginMonthly: item.grossMarginMonthly,
    linkDescription: item.linkDescription ? item.linkDescription : "N/A",
    current_portfolio: item.current_portfolio === 1 ? dictionary["yes"] : dictionary["no"],
  }));
}, []);

//Example of one handler
const handleFilterModelChange = useCallback((model) => {
     //  ⚠️Here we dispatch some action, or make some state changes for this component. If we do so, the table loss all states, and revert to the initial state.
  }, []);

return (
  <BaseBox
    sx={{
      height: "100%",
      width: "100%",
      "& .MuiDataGrid-cell.rating-a": {
        color: theme?.palette.semantic.success.veryhigh,
      },
      "& .MuiDataGrid-cell.rating-c": {
        color: theme?.palette.semantic.danger.veryhigh,
      },
    }}
  >
    <BaseTable
      data={mappedArray}
      columns={columns}
      handleFilterModelChange={handleFilterModelChange}
    />
  </BaseBox>
);

And here the definition of the BaseTable component where is define the DataGrid MUI x component


const BaseTable = ({
  data = [],
  columns = [],
  columnsToHide = [],
  handleColumnOrderChange = () => {},
  handleSortModelChange = () => {},
  handlePinnedColumnsChange = () => {},
  handleGroupModelChange = () => {},
  handleFilterModelChange = () => {},
  handleRowSelectionChange = () => {},
}) => {
  const apiRef = useGridApiRef();
  const theme = useTheme();

  return (
    <DataGridPremium
      apiRef={apiRef}
      rows={data}
      columns={columns}
      pagination
      pageSizeOptions={[25, 50, 100]}
      onColumnHeaderClick={(params, event) => {
        event.defaultMuiPrevented = true;
        apiRef.current.showColumnMenu(params.field);
      }}
      onRowGroupingModelChange={(newModel) => {
        handleGroupModelChange(newModel);
      }}
      onSortModelChange={(model) => {
        handleSortModelChange(model);
      }}
      onFilterModelChange={(model) => {
        handleFilterModelChange(model);
      }}
      onColumnOrderChange={(params) => {
        handleColumnOrderChange(params);
      }}
      onPinnedColumnsChange={(model) => {
        handlePinnedColumnsChange(model);
      }}
      onRowSelectionModelChange={(model) => {
        handleRowSelectionChange(model);
      }}
      sx={{
        ".MuiDataGrid-groupingCriteriaCellToggle": {
          marginRight: "0px",
        },
        ".MuiDataGrid-columnHeader .MuiButtonBase-root.MuiIconButton-root": {
          padding: 0,
          width: 0,
          bgcolor: "transparent",
        },
        ".MuiDataGrid-columnHeader--sorted .MuiButtonBase-root.MuiIconButton-root, .MuiDataGrid-columnHeader--filtered .MuiButtonBase-root.MuiIconButton-root":
          {
            paddingLeft: "6px",
            paddingRight: "10px",
          },
        ".MuiDataGrid-columnHeader:not(.MuiDataGrid-columnHeader--sorted).MuiDataGrid-columnHeader--filtered .MuiButtonBase-root.MuiIconButton-root:has(.MuiDataGrid-sortIcon)":
          {
            padding: 0,
            width: 0,
          },
        ".MuiBadge-badge": {
          top: "-3px",
          right: "3px",
          color: theme.palette.light.content.mid,
        },
      }}
    />
  );
};

BaseTable.propTypes = {
  data: PropTypes.array.isRequired,
  columns: PropTypes.array.isRequired,
  tableName: PropTypes.string.isRequired,
};

export default BaseTable;

Your environment

   Browser: Chrome ,

    System:
    OS: Windows 11 10.0.22631
  Binaries:
    Node: 18.16.1 - C:\Program Files\nodejs\node.EXE
    npm: 9.5.1 - C:\Program Files\nodejs\npm.CMD
    pnpm: Not Found
  Browsers:
    Chrome: Not Found
    Edge: Chromium (128.0.2739.54)
  npmPackages:
    @emotion/react: ^11.11.4 => 11.11.4 
    @emotion/styled: ^11.11.5 => 11.11.5 
    @mui/base:  5.0.0-beta.40 
    @mui/core-downloads-tracker:  5.15.20 
    @mui/icons-material: ^5.15.20 => 5.15.20 
    @mui/material: ^5.15.17 => 5.15.20 
    @mui/private-theming:  5.16.1 
    @mui/styled-engine:  5.16.1
    @mui/system:  5.16.1
    @mui/types:  7.2.15
    @mui/utils:  5.16.1
    @mui/x-data-grid:  7.10.0
    @mui/x-data-grid-generator:  7.10.0
    @mui/x-data-grid-premium: ^7.7.0 => 7.10.0
    @mui/x-data-grid-pro:  7.10.0
    @mui/x-internals:  7.10.0
    @mui/x-license: ^7.6.1 => 7.10.0
    @types/react:  18.3.3
    react: ^18.3.1 => 18.3.1
    react-dom: ^18.3.1 => 18.3.1

Search keywords: Preserve State Filters Dimensions Order ID: 92280

michelengelen commented 1 month ago

To help us diagnose the issue efficiently, could you provide a stripped-down reproduction test case using the latest version? A live example would be fantastic! ✨

For your convenience, our documentation offers templates and guides on creating targeted examples: Support - Bug reproduction

Just a friendly reminder: clean, functional code with minimal dependencies is most helpful. Complex code can make it tougher to pinpoint the exact issue. Sometimes, simply going through the process of creating a minimal reproduction can even clarify the problem itself!

Thanks for your understanding! 🙇🏼

github-actions[bot] commented 1 month ago

The issue has been inactive for 7 days and has been automatically closed.