Closed snarky-barnacle closed 1 month ago
@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
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?
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
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?
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....
@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>
);
}
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>
);
}
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.
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.
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 thegetActions
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: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 inspectapiRef.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
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