-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Conversation
These are the results for the performance tests:
|
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
48a4b8f
to
95736ad
Compare
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
0f87516
to
0562193
Compare
@@ -27,13 +28,15 @@ const DataGridProRaw = React.forwardRef(function DataGridPro<R extends GridValid | |||
const privateApiRef = useDataGridProComponent(props.apiRef, props); | |||
useLicenseVerifier('x-data-grid-pro', releaseInfo); | |||
|
|||
const pinnedColumns = useGridSelector(privateApiRef, gridPinnedColumnsSelector); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pinned columns must be passed via props now otherwise, once the column headers are memoized, they won't reflect the changes.
const visibleColumns = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector); | ||
const columnPositions = useGridSelector(apiRef, gridColumnPositionsSelector); | ||
const columnHeaderTabIndexState = useGridSelector(apiRef, gridTabIndexColumnHeaderSelector); | ||
const cellTabIndexState = useGridSelector(apiRef, gridTabIndexCellSelector); | ||
const columnGroupHeaderTabIndexState = useGridSelector( | ||
apiRef, | ||
unstable_gridTabIndexColumnGroupHeaderSelector, | ||
); | ||
const columnHeaderFocus = useGridSelector(apiRef, gridFocusColumnHeaderSelector); | ||
const columnGroupHeaderFocus = useGridSelector( | ||
apiRef, | ||
unstable_gridFocusColumnGroupHeaderSelector, | ||
); | ||
const densityFactor = useGridSelector(apiRef, gridDensityFactorSelector); | ||
const headerGroupingMaxDepth = useGridSelector(apiRef, gridColumnGroupsHeaderMaxDepthSelector); | ||
const filterColumnLookup = useGridSelector(apiRef, gridFilterActiveItemsLookupSelector); | ||
const sortColumnLookup = useGridSelector(apiRef, gridSortColumnLookupSelector); | ||
const columnMenuState = useGridSelector(apiRef, gridColumnMenuSelector); | ||
const columnVisibility = useGridSelector(apiRef, gridColumnVisibilityModelSelector); | ||
const columnGroupsHeaderStructure = useGridSelector( | ||
apiRef, | ||
gridColumnGroupsHeaderStructureSelector, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using selectors may cause problems if the component is wrapped in React.memo
. The solution is to pass the selected value via props, which makes the component to re-render if the value changes.
if (invalidatesCachedRenderedColumns) { | ||
cachedRenderedColumns.current = visibleColumns.slice(firstColumnToRender, lastColumnToRender); | ||
prevFirstColumnToRender.current = firstColumnToRender; | ||
prevLastColumnToRender.current = lastColumnToRender; | ||
prevVisibleColumns.current = visibleColumns; | ||
} | ||
|
||
const renderedColumns = cachedRenderedColumns.current; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous approach with a simple Array.slice
was making the renderedColumns
reference to change in every render. Here I created a small cache for it.
style={{ | ||
...rowStyle, | ||
...rootRowStyle, | ||
}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If kept like that, the component will never be memoized because style
is always changing. To support React.memo
, the styles are computed once and saved to a cache.
/>; | ||
``` | ||
|
||
The following demo show this trick in action. |
There was a problem hiding this comment.
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.
{{"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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we memoize the Cell
slot though?
renderCell
is called in GridRow
component and the returned value is passed to the GridCell
.
So if there's renderCell
in the column - the cell will always rerender - see this demo: https://codesandbox.io/s/dreamy-roentgen-px945x
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, I added React.memo
to the cell component by default now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about this more, wouldn't it make more sense to call renderCell
inside the GridCell
component?
This should allow memoizing the GridRow
by default (as opposed to GridCell
), and then the GridCell
can be memoized by users. (Maybe then we could go a step further and add a check in React.memo
to memoize GridCell
by default if colDef.renderCell
is defined?).
If it's too time-consuming, we can work on this later - this PR is already a great improvement!
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we add this check to React.memo
we lose the default shallow comparison function. I don't know what's the best approach here. I propose to leave this as it is and see the implications.
{{"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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way, is there a way to reduce rerenders of the cells that have renderCell
? Would memoizing the component returned by renderCell
work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would memoizing the component returned by renderCell work?
I tested and it didn't work. To memoize cells with custom renderers, we should call renderCell
and cache its content, then invalidate the cached value if some prop changes. If we use the row
prop as cache key, we fall again into the problem of the cell renderer depending on a selector. It's better to leave this for the user to cache the value. We can do a follow-up explaining how to do this.
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
sx={{ | ||
height: 400, | ||
width: '100%', | ||
'& .updating': { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'& .updating': { | |
'&& .updating': { |
To override the row hover background color, otherwise it might look like the row wasn't rerendered
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
import { | ||
GridRow, | ||
DataGrid, // or DataGridPro, DataGridPremium | ||
DataGridColumnHeaders, // or DataGridProColumnHeaders, DataGridPremiumColumnHeaders |
There was a problem hiding this comment.
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?
return <TraceUpdates ref={ref} Component={DataGridProColumnHeaders} {...props} />; | ||
}); | ||
|
||
const MemoizedRow = React.memo(RowWithTracer); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
{{"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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about this more, wouldn't it make more sense to call renderCell
inside the GridCell
component?
This should allow memoizing the GridRow
by default (as opposed to GridCell
), and then the GridCell
can be memoized by users. (Maybe then we could go a step further and add a check in React.memo
to memoize GridCell
by default if colDef.renderCell
is defined?).
If it's too time-consuming, we can work on this later - this PR is already a great improvement!
What do you think?
packages/grid/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great improvement 🚀
docs/data/migration/migration-data-grid-v5/migration-data-grid-v5.md
Outdated
Show resolved
Hide resolved
docs/data/migration/migration-data-grid-v5/migration-data-grid-v5.md
Outdated
Show resolved
Hide resolved
Co-authored-by: Andrew Cherniavskii <andrew.cherniavskii@gmail.com> Signed-off-by: Matheus Wichman <matheushw@outlook.com>
'&&& .updating': (theme) => ({ | ||
background: teal[theme.palette.mode === 'dark' ? 900 : 100], | ||
transition: theme.transitions.create('background', { | ||
duration: theme.transitions.duration.standard, | ||
}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love the idea, but I think that we could revisit the animation. Done in #8986 😁
First step for #7807
Preview: https://deploy-preview-7846--material-ui-x.netlify.app/x/react-data-grid/performance/
Changelog
Breaking changes
cellFocus
,cellTabIndex
andeditRowsState
props are not passed to the component used in the row slot. You can use the newfocusedCell
andtabbableCell
props instead. For the editing state, use the API methods.