Skip to content

Commit

Permalink
[DataGrid] Make possible to memoize rows and cells
Browse files Browse the repository at this point in the history
  • Loading branch information
m4theushw committed Feb 15, 2023
1 parent a7ed810 commit 0562193
Show file tree
Hide file tree
Showing 36 changed files with 923 additions and 293 deletions.
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);

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,
}}
/>
81 changes: 81 additions & 0 deletions docs/data/data-grid/performance/GridWithReactMemo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { unstable_useForkRef as useForkRef } from '@mui/utils';
import {
DataGridPro,
GridRow,
GridCell,
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 CellWithTracer = React.forwardRef((props, ref) => {
return <TraceUpdates ref={ref} Component={GridCell} {...props} />;
});

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

const MemoizedRow = React.memo(RowWithTracer);
const MemoizedCell = React.memo(CellWithTracer);
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
components={{
Row: MemoizedRow,
Cell: MemoizedCell,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
</Box>
);
}
81 changes: 81 additions & 0 deletions docs/data/data-grid/performance/GridWithReactMemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { unstable_useForkRef as useForkRef } from '@mui/utils';
import {
DataGridPro,
GridRow,
GridCell,
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 CellWithTracer = React.forwardRef((props, ref) => {
return <TraceUpdates ref={ref} Component={GridCell} {...props} />;
});

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

const MemoizedRow = React.memo(RowWithTracer);
const MemoizedCell = React.memo(CellWithTracer);
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
components={{
Row: MemoizedRow,
Cell: MemoizedCell,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
</Box>
);
}
11 changes: 11 additions & 0 deletions docs/data/data-grid/performance/GridWithReactMemo.tsx.preview
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<DataGridPro
{...data}
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
components={{
Row: MemoizedRow,
Cell: MemoizedCell,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
72 changes: 72 additions & 0 deletions docs/data/data-grid/performance/performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 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.
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).
One way to overcome this issue is using `React.memo` to only re-render the children components when their props have changed.
To start using memoization, import the inner components, then pass their memoized version to the respective slots, as follow:

```tsx
import {
GridRow,
GridCell,
DataGrid, // or DataGridPro, DataGridPremium
DataGridColumnHeaders, // or DataGridProColumnHeaders, DataGridPremiumColumnHeaders
} from '@mui/x-data-grid';

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

<DataGrid
components={{
Row: MemoizedRow,
Cell: MemoizedCell,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>;
```

The following demo show this trick in action.
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 cells that display custom content whose source is not the received props, these cells may display outdated information.
For instance, 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 `GridCell` and `GridRow`, the row and column should not be memoized.
You can choose whether to memoize or not a component by passing a 2nd argument to `React.memo`:

```tsx
function shallowCompare(prevProps, nextProps) {
const aKeys = Object.keys(prevProps);
const bKeys = Object.keys(nextProps);

if (aKeys.length !== bKeys.length) {
return false;
}

return aKeys.every(
(key) => nextProps.hasOwnProperty(key) && nextProps[key] === prevProps[key],
);
}

const MemoizedCell = React.memo(GridCell, (prevProps, nextProps) => {
// Prevent memoizing the cells from the "total" column
return nextProps.field !== 'total' && shallowCompare(prevProps, nextProps);
});
```

:::

## 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 @@ -390,13 +390,13 @@ 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.
You can copy the old function and pass it to the `getApplyQuickFilterFn` property of the `singleSelect` column definition.

### 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.
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 @@ -263,6 +263,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 @@ -238,6 +238,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 @@ -183,6 +183,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 @@ -698,6 +698,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
Loading

0 comments on commit 0562193

Please sign in to comment.