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

[DataGrid] Make possible to memoize rows and cells #7846

Merged
merged 17 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs/data/data-grid/overview/DataGridProDemo.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { DataGridPro } from '@mui/x-data-grid-pro';
import {
DataGridPro,
GridRow,
GridCell,
DataGridProColumnHeaders,
} from '@mui/x-data-grid-pro';
import { useDemoData } from '@mui/x-data-grid-generator';

const MemoizedRow = React.memo(GridRow);

const MemoizedCell = React.memo(GridCell);
m4theushw marked this conversation as resolved.
Show resolved Hide resolved

const MemoizedColumnHeaders = React.memo(DataGridProColumnHeaders);

export default function DataGridProDemo() {
const { data } = useDemoData({
dataSet: 'Commodity',
Expand All @@ -18,6 +29,11 @@ export default function DataGridProDemo() {
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
components={{
Row: MemoizedRow,
Cell: MemoizedCell,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
</Box>
);
Expand Down
18 changes: 17 additions & 1 deletion docs/data/data-grid/overview/DataGridProDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { DataGridPro } from '@mui/x-data-grid-pro';
import {
DataGridPro,
GridRow,
GridCell,
DataGridProColumnHeaders,
} from '@mui/x-data-grid-pro';
import { useDemoData } from '@mui/x-data-grid-generator';

const MemoizedRow = React.memo(GridRow);

const MemoizedCell = React.memo(GridCell);

const MemoizedColumnHeaders = React.memo(DataGridProColumnHeaders);

export default function DataGridProDemo() {
const { data } = useDemoData({
dataSet: 'Commodity',
Expand All @@ -18,6 +29,11 @@ export default function DataGridProDemo() {
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
components={{
Row: MemoizedRow,
Cell: MemoizedCell,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
</Box>
);
Expand Down
5 changes: 5 additions & 0 deletions docs/data/data-grid/overview/DataGridProDemo.tsx.preview
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
components={{
Row: MemoizedRow,
Cell: MemoizedCell,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
74 changes: 74 additions & 0 deletions docs/data/data-grid/performance/GridWithReactMemo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { unstable_useForkRef as useForkRef } from '@mui/utils';
import {
DataGridPro,
GridRow,
DataGridProColumnHeaders,
} from '@mui/x-data-grid-pro';
import { useDemoData } from '@mui/x-data-grid-generator';

const TraceUpdates = React.forwardRef((props, ref) => {
const { Component, ...other } = props;
const rootRef = React.useRef();
const handleRef = useForkRef(rootRef, ref);

React.useEffect(() => {
rootRef.current?.classList.add('updating');

const timer = setTimeout(() => {
rootRef.current?.classList.remove('updating');
}, 500);

return () => clearTimeout(timer);
});

return <Component ref={handleRef} {...other} />;
});

const RowWithTracer = React.forwardRef((props, ref) => {
return <TraceUpdates ref={ref} Component={GridRow} {...props} />;
});

const ColumnHeadersWithTracer = React.forwardRef((props, ref) => {
return <TraceUpdates ref={ref} Component={DataGridProColumnHeaders} {...props} />;
});

const MemoizedRow = React.memo(RowWithTracer);
const MemoizedColumnHeaders = React.memo(ColumnHeadersWithTracer);

export default function GridWithReactMemo() {
const { data } = useDemoData({
dataSet: 'Commodity',
rowLength: 100,
editable: true,
maxColumns: 15,
});

return (
<Box
sx={{
height: 400,
width: '100%',
'& .updating': {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'& .updating': {
'&& .updating': {

To override the row hover background color, otherwise it might look like the row wasn't rerendered

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's still not enough - &&& is needed to actually override the hover color

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand. The row doesn't appear to be rendering again on hover.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I managed to reproduce the bug, it's tricky to see it.

background: '#b2dfdb',
cherniavskii marked this conversation as resolved.
Show resolved Hide resolved
transition: (theme) =>
theme.transitions.create('background', {
duration: theme.transitions.duration.standard,
}),
},
}}
>
<DataGridPro
{...data}
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
slots={{
row: MemoizedRow,
columnHeaders: MemoizedColumnHeaders,
}}
/>
</Box>
);
}
74 changes: 74 additions & 0 deletions docs/data/data-grid/performance/GridWithReactMemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { unstable_useForkRef as useForkRef } from '@mui/utils';
import {
DataGridPro,
GridRow,
DataGridProColumnHeaders,
} from '@mui/x-data-grid-pro';
import { useDemoData } from '@mui/x-data-grid-generator';

const TraceUpdates = React.forwardRef<any, any>((props, ref) => {
const { Component, ...other } = props;
const rootRef = React.useRef<HTMLElement>();
const handleRef = useForkRef(rootRef, ref);

React.useEffect(() => {
rootRef.current?.classList.add('updating');

const timer = setTimeout(() => {
rootRef.current?.classList.remove('updating');
}, 500);

return () => clearTimeout(timer);
});

return <Component ref={handleRef} {...other} />;
});

const RowWithTracer = React.forwardRef((props, ref) => {
return <TraceUpdates ref={ref} Component={GridRow} {...props} />;
});

const ColumnHeadersWithTracer = React.forwardRef((props, ref) => {
return <TraceUpdates ref={ref} Component={DataGridProColumnHeaders} {...props} />;
});

const MemoizedRow = React.memo(RowWithTracer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expected that the whole row is rerendered on cell focus change?

Screen.Recording.2023-02-20.at.19.52.49.mov

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because which cell is focused comes from the focusedCell prop. If I remove this prop and pass it to the cell, from inside the row, then the row won't re-render because no prop has changed, even though the props passed to the cell will have changed.

const MemoizedColumnHeaders = React.memo(ColumnHeadersWithTracer);

export default function GridWithReactMemo() {
const { data } = useDemoData({
dataSet: 'Commodity',
rowLength: 100,
editable: true,
maxColumns: 15,
});

return (
<Box
sx={{
height: 400,
width: '100%',
'& .updating': {
background: '#b2dfdb',
transition: (theme) =>
theme.transitions.create('background', {
duration: theme.transitions.duration.standard,
}),
},
}}
>
<DataGridPro
{...data}
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
slots={{
row: MemoizedRow,
columnHeaders: MemoizedColumnHeaders,
}}
/>
</Box>
);
}
10 changes: 10 additions & 0 deletions docs/data/data-grid/performance/GridWithReactMemo.tsx.preview
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<DataGridPro
{...data}
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
slots={{
row: MemoizedRow,
columnHeaders: MemoizedColumnHeaders,
}}
/>
47 changes: 47 additions & 0 deletions docs/data/data-grid/performance/performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Data Grid - Performance

<p class="description">Improve the performance of the DataGrid using the recommendations from this guide.</p>

## Memoize inner components with `React.memo`

The `DataGrid` component is composed of a central state object where all data is stored.
When an API method is called, a prop changes, or the user interacts with the UI (e.g. filtering a column), this state object is updated with the changes made.
To reflect the changes in the interface, the component must re-render.
Since the state behaves like `React.useState`, it means that the `DataGrid` component will re-render as well as its children, which includes column headers, rows and cells.
m4theushw marked this conversation as resolved.
Show resolved Hide resolved
With smaller datasets, this is not a problem for concern, but it can become a bottleneck if the number of rows gets increased and, specially, if many columns render [custom content](/x/react-data-grid/column-definition/#rendering-cells).
m4theushw marked this conversation as resolved.
Show resolved Hide resolved
One way to overcome this issue is using `React.memo` to only re-render the children components when their props have changed.
m4theushw marked this conversation as resolved.
Show resolved Hide resolved
To start using memoization, import the inner components, then pass their memoized version to the respective slots, as follow:

```tsx
import {
GridRow,
DataGrid, // or DataGridPro, DataGridPremium
DataGridColumnHeaders, // or DataGridProColumnHeaders, DataGridPremiumColumnHeaders
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note: This seems inconsistent with the other components' names that do not change regardless of the package used (one exception is DataGrid/DataGridPro/DataGridPremium).
Should we rename it to GridColumnHeaders for all packages?

} from '@mui/x-data-grid';

const MemoizedRow = React.memo(GridRow);
const MemoizedColumnHeaders = React.memo(DataGridColumnHeaders);

<DataGrid
slots={{
row: MemoizedRow,
columnHeaders: MemoizedColumnHeaders,
}}
/>;
```

The following demo show this trick in action.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't add a "before" demo because the comparison would be too ridiculous but here's https://codesandbox.io/s/crimson-tdd-fb0vec?file=/demo.tsx. Basically, in any click everything re-renders.

It also contains additional logic to highlight the components when they re-render.

{{"demo": "GridWithReactMemo.js", "bg": "inline", "defaultCodeOpen": false}}

:::warning
We do not ship the components above already wrapped with `React.memo` because if you have rows whose cells display custom content not derived from the received props, e.g. selectors, these cells may display outdated information.
If you define a column with a custom cell renderer where content comes from a [selector](/x/react-data-grid/state/#catalog-of-selectors) that changes more often then the props passed to `GridRow`, the row component should not be memoized.
m4theushw marked this conversation as resolved.
Show resolved Hide resolved
:::

## API

- [DataGrid](/x/api/data-grid/data-grid/)
- [DataGridPro](/x/api/data-grid/data-grid-pro/)
- [DataGridPremium](/x/api/data-grid/data-grid-premium/)
Original file line number Diff line number Diff line change
Expand Up @@ -440,12 +440,6 @@ Most of this breaking change is handled by `preset-safe` codemod but some furthe
| `.MuiDataGrid-withBorder` | `.MuiDataGrid-withBorderColor` | The class only sets `border-color` CSS property |
| `.MuiDataGrid-filterFormLinkOperatorInput` | `.MuiDataGrid-filterFormLogicOperatorInput` | |

<!--
### Virtualization

TBD
-->

### Removals from the public API

- The `getGridSingleSelectQuickFilterFn` function was removed.
Expand All @@ -464,3 +458,9 @@ npx @mui/x-codemod v6.0.0/data-grid/rename-components-to-slots <path>
```

Take a look at [the RFC](https://github.com/mui/material-ui/issues/33416) for more information.

### Misc

- The `cellFocus`, `cellTabIndex` and `editRowsState` props are not passed to the component used in the row slot.
You can use the new `focusedCell` and `tabbableCell` props instead.
For the editing state, use the API methods.
m4theushw marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions docs/data/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const pages: MuiPage[] = [
{ pathname: '/x/react-data-grid/scrolling' },
{ pathname: '/x/react-data-grid/virtualization' },
{ pathname: '/x/react-data-grid/accessibility' },
{ pathname: '/x/react-data-grid/performance' },
{
pathname: '/x/react-data-grid-group-pivot',
title: 'Group & Pivot',
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/data-grid/data-grid-premium.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@
"default": "GridColumnHeaderFilterIconButton",
"type": { "name": "elementType" }
},
"columnHeaders": { "default": "DataGridColumnHeaders", "type": { "name": "elementType" } },
"columnMenu": { "default": "GridColumnMenu", "type": { "name": "elementType" } },
"columnMenuAggregationIcon": {
"default": "GridFunctionsIcon",
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/data-grid/data-grid-pro.json
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@
"default": "GridColumnHeaderFilterIconButton",
"type": { "name": "elementType" }
},
"columnHeaders": { "default": "DataGridColumnHeaders", "type": { "name": "elementType" } },
"columnMenu": { "default": "GridColumnMenu", "type": { "name": "elementType" } },
"columnMenuClearIcon": { "default": "GridClearIcon", "type": { "name": "elementType" } },
"columnMenuFilterIcon": { "default": "GridFilterAltIcon", "type": { "name": "elementType" } },
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/data-grid/data-grid.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@
"default": "GridColumnHeaderFilterIconButton",
"type": { "name": "elementType" }
},
"columnHeaders": { "default": "DataGridColumnHeaders", "type": { "name": "elementType" } },
"columnMenu": { "default": "GridColumnMenu", "type": { "name": "elementType" } },
"columnMenuClearIcon": { "default": "GridClearIcon", "type": { "name": "elementType" } },
"columnMenuFilterIcon": { "default": "GridFilterAltIcon", "type": { "name": "elementType" } },
Expand Down
7 changes: 7 additions & 0 deletions docs/pages/x/react-data-grid/performance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as React from 'react';
import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs';
import * as pageProps from 'docsx/data/data-grid/performance/performance.md?@mui/markdown';

export default function Page() {
return <MarkdownDocs {...pageProps} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,7 @@
"cell": "Component rendered for each cell.",
"columnFilteredIcon": "Icon displayed on the column header menu to show that a filter has been applied to the column.",
"columnHeaderFilterIconButton": "Filter icon component rendered in each column header.",
"columnHeaders": "Component responsible for rendering the column headers.",
"columnMenu": "Column menu component rendered by clicking on the 3 dots &quot;kebab&quot; icon in column headers.",
"columnMenuAggregationIcon": "Icon displayed in column menu for aggregation",
"columnMenuClearIcon": "Icon displayed in column menu for clearing values",
Expand Down
1 change: 1 addition & 0 deletions docs/translations/api-docs/data-grid/data-grid-pro.json
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@
"cell": "Component rendered for each cell.",
"columnFilteredIcon": "Icon displayed on the column header menu to show that a filter has been applied to the column.",
"columnHeaderFilterIconButton": "Filter icon component rendered in each column header.",
"columnHeaders": "Component responsible for rendering the column headers.",
"columnMenu": "Column menu component rendered by clicking on the 3 dots &quot;kebab&quot; icon in column headers.",
"columnMenuClearIcon": "Icon displayed in column menu for clearing values",
"columnMenuFilterIcon": "Icon displayed in column menu for filter",
Expand Down
1 change: 1 addition & 0 deletions docs/translations/api-docs/data-grid/data-grid.json
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,7 @@
"cell": "Component rendered for each cell.",
"columnFilteredIcon": "Icon displayed on the column header menu to show that a filter has been applied to the column.",
"columnHeaderFilterIconButton": "Filter icon component rendered in each column header.",
"columnHeaders": "Component responsible for rendering the column headers.",
"columnMenu": "Column menu component rendered by clicking on the 3 dots &quot;kebab&quot; icon in column headers.",
"columnMenuClearIcon": "Icon displayed in column menu for clearing values",
"columnMenuFilterIcon": "Icon displayed in column menu for filter",
Expand Down
Loading