Skip to content

Commit

Permalink
[DataGrid] Make possible to memoize rows and cells (#7846)
Browse files Browse the repository at this point in the history
Signed-off-by: Matheus Wichman <[email protected]>
Co-authored-by: Andrew Cherniavskii <[email protected]>
  • Loading branch information
m4theushw and cherniavskii authored Feb 27, 2023
1 parent 17dd6eb commit 13c7b10
Show file tree
Hide file tree
Showing 41 changed files with 1,010 additions and 446 deletions.
10 changes: 9 additions & 1 deletion docs/data/data-grid/overview/DataGridProDemo.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { DataGridPro } from '@mui/x-data-grid-pro';
import { DataGridPro, GridRow, GridColumnHeaders } from '@mui/x-data-grid-pro';
import { useDemoData } from '@mui/x-data-grid-generator';

const MemoizedRow = React.memo(GridRow);

const MemoizedColumnHeaders = React.memo(GridColumnHeaders);

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

const MemoizedRow = React.memo(GridRow);

const MemoizedColumnHeaders = React.memo(GridColumnHeaders);

export default function DataGridProDemo() {
const { data } = useDemoData({
dataSet: 'Commodity',
Expand All @@ -18,6 +22,10 @@ export default function DataGridProDemo() {
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
components={{
Row: MemoizedRow,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
</Box>
);
Expand Down
4 changes: 4 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,8 @@
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
components={{
Row: MemoizedRow,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
70 changes: 70 additions & 0 deletions docs/data/data-grid/performance/GridWithReactMemo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { teal } from '@mui/material/colors';
import { unstable_useForkRef as useForkRef } from '@mui/utils';
import { DataGridPro, GridRow, GridColumnHeaders } 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={GridColumnHeaders} {...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': (theme) => ({
background: teal[theme.palette.mode === 'dark' ? 900 : 100],
transition: theme.transitions.create('background', {
duration: theme.transitions.duration.standard,
}),
}),
}}
>
<DataGridPro
{...data}
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
slots={{
row: MemoizedRow,
columnHeaders: MemoizedColumnHeaders,
}}
/>
</Box>
);
}
70 changes: 70 additions & 0 deletions docs/data/data-grid/performance/GridWithReactMemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { teal } from '@mui/material/colors';
import { unstable_useForkRef as useForkRef } from '@mui/utils';
import { DataGridPro, GridRow, GridColumnHeaders } 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={GridColumnHeaders} {...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': (theme) => ({
background: teal[theme.palette.mode === 'dark' ? 900 : 100],
transition: 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`, the `DataGrid` component will re-render its children, including 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 increases, especially 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 child 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,
GridColumnHeaders,
DataGrid, // or DataGridPro, DataGridPremium
} from '@mui/x-data-grid';

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

<DataGrid
slots={{
row: MemoizedRow,
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 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 than the props passed to `GridRow`, the row component should not be memoized.
:::

## 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 @@ -270,6 +270,17 @@ Most of this breaking change is handled by `preset-safe` codemod but some furthe
- The `GridActionsCellProps['api']` property was removed. Use `useGridApiContext` hook instead to get `apiRef`.
- The `GridActionsCellProps['getValue']` property was removed. Use `params.row` instead.
- The `GridFooterCellProps['getValue']` property was removed. Use `params.row` instead.
- The `cellFocus`, `cellTabIndex` and `editRowsState` props are not passed to the `Row` slot anymore.
Use the `focusedCell` and `tabbableCell` props instead.
For the editing state, use the API methods.
```diff
const CustomRow = (props) => {
- const focusedField = props.cellFocus.field;
+ const focusedField = props.focusedCell;
- const tabIndex = props.cellTabIndex.field && cellMode === 'view' ? 0 : 1;
+ const tabIndex = props.tabbableCell === column.field ? 0 : 1;
}
```

### Pagination

Expand Down Expand Up @@ -449,12 +460,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 Down
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 @@ -274,6 +274,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 @@ -701,6 +701,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
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import {
GridRoot,
GridContextProvider,
GridValidRowModel,
useGridSelector,
gridPinnedColumnsSelector,
} from '@mui/x-data-grid-pro';
import {
DataGridProVirtualScroller,
DataGridProColumnHeaders,
} from '@mui/x-data-grid-pro/internals';
import { DataGridProVirtualScroller } from '@mui/x-data-grid-pro/internals';
import { useDataGridPremiumComponent } from './useDataGridPremiumComponent';
import { DataGridPremiumProps } from '../models/dataGridPremiumProps';
import { useDataGridPremiumProps } from './useDataGridPremiumProps';
Expand All @@ -30,13 +29,15 @@ const DataGridPremiumRaw = React.forwardRef(function DataGridPremium<R extends G

useLicenseVerifier('x-data-grid-premium', releaseInfo);

const pinnedColumns = useGridSelector(privateApiRef, gridPinnedColumnsSelector);

return (
<GridContextProvider privateApiRef={privateApiRef} props={props}>
<GridRoot className={props.className} style={props.style} sx={props.sx} ref={ref}>
<GridHeader />
<GridBody
ColumnHeadersComponent={DataGridProColumnHeaders}
VirtualScrollerComponent={DataGridProVirtualScroller}
ColumnHeadersProps={{ pinnedColumns }}
>
<Watermark packageName="x-data-grid-premium" releaseInfo={releaseInfo} />
</GridBody>
Expand Down
2 changes: 2 additions & 0 deletions packages/grid/x-data-grid-premium/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export * from './hooks';
export * from './models';
export * from './components';

export { GridColumnHeaders } from '@mui/x-data-grid-pro';

export type {
DataGridPremiumProps,
GridExperimentalPremiumFeatures,
Expand Down
Loading

0 comments on commit 13c7b10

Please sign in to comment.