Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[data grid] Problem getting latest state of row in edit mode: Have there been changes to how the gridEditRowsStateSelector() works? #14967

Closed
snarky-barnacle opened this issue Oct 14, 2024 · 9 comments
Assignees
Labels
component: data grid This is the name of the generic UI component, not the React module! support: commercial Support request from paid users support: pro standard Support request from a Pro standard plan user. https://mui.com/legal/technical-support-sla/

Comments

@snarky-barnacle
Copy link

snarky-barnacle commented Oct 14, 2024

The problem in depth

We recently updated our version of the DataGridPro to 7.20.0 from v5. In doing so, we have found that one of the selectors we use to do custom validation and control of the row mode no longer works as expected. We think we may have found a bug in the DataGridPro component, but wondering if there's another way to accomplish what we're trying to do?

In our application we use row editing, and have edit/save buttons in an actions column to control the row mode. We only allow saving row edits if all cell validation criteria have been met.
To do this, we use gridEditRowsStateSelector() as part of the getActions function in the columns definition. We get the latest state of the edited row, then use a validation utility on that state that returns true/false, allowing us to enable/disable the save button, like so:

...
getActions: (params: GridRowParams<Article>) => {
        const isInEditMode =
          rowModesModel[params.id]?.mode === GridRowModes.Edit;

        const rowHasEmptyFields = (editRow: GridEditRowProps) =>
          !editRow?.author.value ||
          !editRow?.articleTitle.value ||
          !editRow?.rating.value;

        console.log(gridEditRowsStateSelector(apiRef.current.state)[params.id]);
        if (isInEditMode) {
          return [
            <GridActionsCellItem
              key="Save"
              icon={<CheckIcon />}
              label="Save"
              onClick={handleSaveClick(params.id)}
              disabled={rowHasEmptyFields(
                gridEditRowsStateSelector(apiRef.current.state)[params.id]
              )}
            />,
          ];
        }
        return [
          <GridActionsCellItem
            icon={<EditIcon />}
            label="Edit"
            key="Edit"
            onClick={handleEditClick(params.id)}
          />,
        ];
      },
...

The problem: we found that when filling in a row, even though all cell criteria are met, the save button would not enable. Upon closer look, we discovered that the DataGrid state wasn't updating after each cell edit--this was done by logging the output gridEditRowsStateSelector() and using React DevTools to inspect apiRef.current.state directly)

I've made a very pared down version DataGridPro demo that has the basic row control functionality we need. It exhibits the same behavior described above: https://stackblitz.com/edit/react-8xnt9e?file=Demo.tsx

Specific steps to reproduce the issue

  1. Click the edit button on the last row
  2. fill in the two empty cells with any text.
  3. Without tabbing out of the last cell you edited, note that the save button has not enabled
  4. Now click on the cell with the save button. Save button should now enable

Due to the complexity of the row editing we have in our application (not evident in this simple demo), it's extremely important that the row state be responsive so the proper feedback can be given to our users.

Your environment

`npx @mui/envinfo`
System:
    OS: macOS 14.6.1
  Binaries:
    Node: 22.4.1 - ~/.nvm/versions/node/v22.4.1/bin/node
    npm: 10.8.1 - ~/.nvm/versions/node/v22.4.1/bin/npm
    pnpm: Not Found
  Browsers:
    Chrome: 129.0.6668.100
    Edge: 129.0.2792.89
    Safari: 17.6
  npmPackages:
    @emotion/react: ^11.9.3 => 11.10.4 
    @emotion/styled: ^11.9.3 => 11.10.4 
    @mui/core-downloads-tracker:  5.16.7 
    @mui/icons-material: ^5.16.7 => 5.16.7 
    @mui/material: ^5.16.7 => 5.16.7 
    @mui/private-theming:  5.16.6 
    @mui/styled-engine:  5.16.6 
    @mui/system:  5.16.7 
    @mui/types:  7.2.16 
    @mui/utils:  5.16.6 
    @mui/x-data-grid:  7.20.0 
    @mui/x-data-grid-pro: ^7.17.0 => 7.20.0 
    @mui/x-date-pickers: ^7.17.0 => 7.17.0 
    @mui/x-internals:  7.17.0 
    @mui/x-license: ^7.16.0 => 7.20.0 
    @types/react: ^18.2.65 => 18.2.65 
    react: ^18.3.1 => 18.3.1 
    react-dom: ^18.3.1 => 18.3.1 
    typescript: ^5.5.4 => 5.5.4 

Browsers used:

  • Microsoft Edge Version 129.0.2792.89
  • Firefox 131.0.2 (aarch64)

Search keywords: row editing, state selector, DataGridPro

@snarky-barnacle snarky-barnacle added status: waiting for maintainer These issues haven't been looked at yet by a maintainer support: commercial Support request from paid users labels Oct 14, 2024
@github-actions github-actions bot added component: data grid This is the name of the generic UI component, not the React module! support: pro standard Support request from a Pro standard plan user. https://mui.com/legal/technical-support-sla/ labels Oct 14, 2024
@arminmeh arminmeh changed the title [question] Problem getting latest state of row in edit mode: Have there been changes to how the gridEditRowsStateSelector() works? [data grid] Problem getting latest state of row in edit mode: Have there been changes to how the gridEditRowsStateSelector() works? Oct 15, 2024
@arminmeh arminmeh self-assigned this Oct 15, 2024
@arminmeh
Copy link
Contributor

@snarky-barnacle

to ensure reactivity, you need to retrieve state updates with useGridSelector hook

If you update getActions to

      getActions: (params: GridRowParams<Article>) => {
        const isInEditMode =
          rowModesModel[params.id]?.mode === GridRowModes.Edit;
        const rowState = useGridSelector(apiRef, gridEditRowsStateSelector)[
          params.id
        ];

        const rowHasEmptyFields = (editRow: GridEditRowProps) =>
          !editRow?.author.value ||
          !editRow?.articleTitle.value ||
          !editRow?.rating.value;

        if (isInEditMode) {
          return [
            <GridActionsCellItem
              key="Save"
              icon={<CheckIcon />}
              label="Save"
              onClick={handleSaveClick(params.id)}
              disabled={rowHasEmptyFields(rowState)}
            />,
          ];
        }
        return [
          <GridActionsCellItem
            icon={<EditIcon />}
            label="Edit"
            key="Edit"
            onClick={handleEditClick(params.id)}
          />,
        ];
      },

your action button should behave as expected.

Hope that this helps

@arminmeh arminmeh added status: waiting for author Issue with insufficient information and removed status: waiting for maintainer These issues haven't been looked at yet by a maintainer labels Oct 15, 2024
@snarky-barnacle
Copy link
Author

Good morning @arminmeh , thanks so much for the reply. Your solution with useGridSelector does indeed fix the behavior, which is great. However, this is triggering our ESLint rules that do not allow the use of react hooks any place else other than functional components (which getActions is not). We get this error:

React Hook "useGridSelector" is called in function "getActions" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use".

I'm working through a solution, but wondering if you have any quick pointers for how to resolve this?

@github-actions github-actions bot added status: waiting for maintainer These issues haven't been looked at yet by a maintainer and removed status: waiting for author Issue with insufficient information labels Oct 15, 2024
@arminmeh
Copy link
Contributor

you can try extracting getActions logic into a component and pass apiRef and params as props

the other possibility is to use renderCell and renderEditCell to return pieces (as components) that you have at the moment in getActions

@arminmeh arminmeh added status: waiting for author Issue with insufficient information and removed status: waiting for maintainer These issues haven't been looked at yet by a maintainer labels Oct 15, 2024
@snarky-barnacle
Copy link
Author

Thanks for the tip--I'm working on extracting the getActions logic into a component, which has led me to extracting const rowState = useGridSelector(apiRef, gridEditRowsStateSelector) into the top level of our functional component. That resolves the hook-use ESLint error, but now I'm getting a type error, which seems to be because apiRef is undefined:

TypeError: Cannot read properties of undefined (reading 'editRows')

we have const apiRef = useGridApiRef(); at the top level of our functional component, just like in the linked demo. Probably something basic I'm forgetting?

@github-actions github-actions bot added status: waiting for maintainer These issues haven't been looked at yet by a maintainer and removed status: waiting for author Issue with insufficient information labels Oct 15, 2024
@snarky-barnacle
Copy link
Author

UPDATE: Just found the place in the docs saying useGridSelector can only be used inside the context of the Data Grid, such as within custom components--which is not what I was doing. Working on another solution....

@snarky-barnacle
Copy link
Author

@arminmeh I believe I'm in the clear now. I was able to implement a custom component for the action buttons and pass the row model, edit/save handlers, and GridRowParams to it. The StackBlitz demo is fighting me right now, so for anyone else that is interested in the solution, the code snippet is below. This code snippet has the proper behavior

I appreciate your quick response to this question--I may return in a little as I have yet to implement in this approach in our actual application, and may run into other issues. I'll update to confirm that all is well.

import { Box } from '@mui/material';
import {
  DataGridPro,
  GridActionsCellItem,
  GridColDef,
  GridEditRowProps,
  gridEditRowsStateSelector,
  GridRowId,
  GridRowModes,
  GridRowModesModel,
  useGridApiContext,
  useGridApiRef,
  useGridSelector,
} from '@mui/x-data-grid-pro';
import CheckIcon from '@mui/icons-material/Check';
import EditIcon from '@mui/icons-material/Edit';
import { useState } from 'react';
import React from 'react';
import { GridRenderCellParams } from '@mui/x-data-grid';

type Article = {
  articleId: string;
  articleTitle: string | null;
  author: string | null;
  rating: 'Excellent' | 'Good' | 'Fair' | 'Poor' | null;
};

function ActionButtons(props: {
  params: GridRenderCellParams<Article>;
  rowModesModel: GridRowModesModel;
  onSaveClick: (id: GridRowId) => void;
  onEditClick: (id: GridRowId) => void;
}) {
  const { params, rowModesModel, onEditClick, onSaveClick } = props;
  const apiRef = useGridApiContext();

  const isInEditMode = rowModesModel[params.id]?.mode === GridRowModes.Edit;

  const rowState = useGridSelector(apiRef, gridEditRowsStateSelector)[
    params.id
  ];

  const rowHasEmptyFields = (editRow: GridEditRowProps) =>
    !editRow?.author.value ||
    !editRow?.articleTitle.value ||
    !editRow?.rating.value;

  if (isInEditMode) {
    return [
      <GridActionsCellItem
        key="Save"
        icon={<CheckIcon />}
        label="Save"
        onClick={() => onSaveClick(params.id)}
        disabled={rowHasEmptyFields(rowState)}
      />,
    ];
  }
  return [
    <GridActionsCellItem
      icon={<EditIcon />}
      label="Edit"
      key="Edit"
      onClick={() => onEditClick(params.id)}
    />,
  ];
}

export default function ExampleDataGrid() {
  const apiRef = useGridApiRef();
  const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});

  const handleEditClick = (id: GridRowId) => {
    console.log('edting');
    setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
  };

  const handleSaveClick = (id: GridRowId) => {
    console.log('saving');
    setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
  };

  // const rowState = useGridSelector(apiRef, gridEditRowsStateSelector);

  const rows: Article[] = [
    {
      articleTitle: 'How to refry beans',
      author: 'John Smith',
      rating: 'Excellent',
      articleId: 'a1',
    },
    {
      articleTitle: 'How to roast asparagus',
      author: 'Angela Myers',
      rating: 'Good',
      articleId: 'b2',
    },
    {
      articleTitle: 'Five times you were wrong about me',
      author: null,
      rating: null,
      articleId: 'c3',
    },
  ];

  const columns: GridColDef[] = [
    {
      field: 'articleTitle',
      headerName: 'Article Title',
      flex: 1,
      editable: true,
    },
    {
      field: 'author',
      headerName: 'Author',
      flex: 1,
      editable: true,
    },
    {
      field: 'rating',
      headerName: 'Rating',
      editable: true,
    },
    {
      field: 'actions',
      type: 'actions',
      renderCell: (params: GridRenderCellParams<Article>) => (
        <ActionButtons
          params={params}
          rowModesModel={rowModesModel}
          onSaveClick={() => handleSaveClick(params.id)}
          onEditClick={() => handleEditClick(params.id)}
        />
      ),
    },
  ];
  return (
    <Box>
      <DataGridPro
        columns={columns}
        rows={rows}
        getRowId={(row: Article) => row.articleId}
        rowModesModel={rowModesModel}
        editMode="row"
        apiRef={apiRef}
        disableRowSelectionOnClick
      />
    </Box>
  );
}

@arminmeh
Copy link
Contributor

pass apiRef

this was a mistake

you can just get it inside the component that you have made (like you did already)

since you already switched to renderCell, you can also use renderEditCell so you don't have to inspect manually inside your action if the row is in edit mode.

Also, splitting the component simplifies the props you need to render them and you can memoize them to prevent unnecessary re-rendering

Here is the re-worked version of your last code

import { Box } from '@mui/material';
import {
  DataGridPro,
  GridActionsCellItem,
  GridColDef,
  gridEditRowsStateSelector,
  GridRowId,
  GridRowModes,
  GridRowModesModel,
  useGridApiContext,
  useGridApiRef,
  useGridSelector,
} from '@mui/x-data-grid-pro';
import CheckIcon from '@mui/icons-material/Check';
import EditIcon from '@mui/icons-material/Edit';
import { useState } from 'react';
import React from 'react';
import { GridRenderCellParams } from '@mui/x-data-grid';

type Article = {
  articleId: string;
  articleTitle: string | null;
  author: string | null;
  rating: 'Excellent' | 'Good' | 'Fair' | 'Poor' | null;
};

function ActionEditButtonRaw(props: { rowId: GridRowId; onClick: (id: GridRowId) => void }) {
  const { rowId, onClick } = props;

  const apiRef = useGridApiContext();
  const rowState = useGridSelector(apiRef, gridEditRowsStateSelector)[rowId];

  const rowHasEmptyFields =
    !rowState?.author.value || !rowState?.articleTitle.value || !rowState?.rating.value;

  return (
    <GridActionsCellItem
      icon={<CheckIcon />}
      label="Save"
      onClick={() => onClick(rowId)}
      disabled={rowHasEmptyFields}
    />
  );
}

const ActionEditButton = React.memo(ActionEditButtonRaw);

function ActionButtonRaw(props: { rowId: GridRowId; onClick: (id: GridRowId) => void }) {
  const { rowId, onClick } = props;

  return <GridActionsCellItem icon={<EditIcon />} label="Edit" onClick={() => onClick(rowId)} />;
}

const ActionButton = React.memo(ActionButtonRaw);

export default function ExampleDataGrid() {
  const apiRef = useGridApiRef();
  const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});

  const handleEditClick = (id: GridRowId) => {
    console.log('edting');
    setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
  };

  const handleSaveClick = (id: GridRowId) => {
    console.log('saving');
    setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
  };

  const rows: Article[] = [
    {
      articleTitle: 'How to refry beans',
      author: 'John Smith',
      rating: 'Excellent',
      articleId: 'a1',
    },
    {
      articleTitle: 'How to roast asparagus',
      author: 'Angela Myers',
      rating: 'Good',
      articleId: 'b2',
    },
    {
      articleTitle: 'Five times you were wrong about me',
      author: null,
      rating: null,
      articleId: 'c3',
    },
  ];

  const columns: GridColDef[] = [
    {
      field: 'articleTitle',
      headerName: 'Article Title',
      flex: 1,
      editable: true,
    },
    {
      field: 'author',
      headerName: 'Author',
      flex: 1,
      editable: true,
    },
    {
      field: 'rating',
      headerName: 'Rating',
      editable: true,
    },
    {
      field: 'actions',
      type: 'actions',
      editable: true,
      renderCell: (params: GridRenderCellParams<Article>) => (
        <ActionButton rowId={params.id} onClick={handleEditClick} />
      ),
      renderEditCell: (params: GridRenderCellParams<Article>) => (
        <ActionEditButton rowId={params.id} onClick={handleSaveClick} />
      ),
    },
  ];
  return (
    <Box>
      <DataGridPro
        columns={columns}
        rows={rows}
        getRowId={(row: Article) => row.articleId}
        rowModesModel={rowModesModel}
        editMode="row"
        apiRef={apiRef}
        disableRowSelectionOnClick
      />
    </Box>
  );
}

@arminmeh arminmeh added status: waiting for author Issue with insufficient information and removed status: waiting for maintainer These issues haven't been looked at yet by a maintainer labels Oct 16, 2024
@snarky-barnacle
Copy link
Author

Nice, appreciate the improvement on the example solution. We are able to move forward now, I'll close this issue. Thank you again for the prompt help.

@github-actions github-actions bot added status: waiting for maintainer These issues haven't been looked at yet by a maintainer and removed status: waiting for author Issue with insufficient information labels Oct 16, 2024
Copy link

This issue has been closed. If you have a similar problem but not exactly the same, please open a new issue.
Now, if you have additional information related to this issue or things that could help future readers, feel free to leave a comment.

Note

@snarky-barnacle How did we do? Your experience with our support team matters to us. If you have a moment, please share your thoughts in this short Support Satisfaction survey.

@github-actions github-actions bot removed the status: waiting for maintainer These issues haven't been looked at yet by a maintainer label Oct 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: data grid This is the name of the generic UI component, not the React module! support: commercial Support request from paid users support: pro standard Support request from a Pro standard plan user. https://mui.com/legal/technical-support-sla/
Projects
None yet
Development

No branches or pull requests

2 participants