-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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] Do not apply filters on rowExpansionChange
#8671
[DataGrid] Do not apply filters on rowExpansionChange
#8671
Conversation
Netlify deploy previewNetlify deploy preview: https://deploy-preview-8671--material-ui-x.netlify.app/ Updated pagesNo updates. These are the results for the performance tests:
|
filteredDescendantCountLookup: {}, | ||
}, | ||
}; | ||
}; | ||
|
||
function useGridVisibleRowsLookup(apiRef: React.MutableRefObject<GridPrivateApiCommunity>) { |
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'm going to move it outside of the useGridFilter
hook, it was just faster to do it like this to validate the approach
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.
Done
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.
Wow, the performance step is impressive! Nice work!
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
packages/grid/x-data-grid/src/hooks/features/rows/gridVisibleRowsSelector.ts
Outdated
Show resolved
Hide resolved
packages/grid/x-data-grid/src/hooks/features/rows/useGridVisibleRowsState.ts
Outdated
Show resolved
Hide resolved
Great work @cherniavskii, it does seem to solve the problem, though I think we would require to apply filtering and aggregation in some use cases, for example when we lazy-load row children, we will need to reapply filtering and aggregation to re-validate the stale data. (But we can do that on demand too, let's say when the promise for fetching new rows is resolved, not sure) After digging a bit into the problem, I found the problem went all the way down to the mui-x/packages/grid/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts Lines 72 to 89 in e9cb67d
One way to solve this could be to only recompute the diff --git a/packages/grid/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts b/packages/grid/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts
index 2a9e12601..d1b2f75b1 100644
--- a/packages/grid/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts
+++ b/packages/grid/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts
@@ -70,21 +70,38 @@ export const useGridAggregation = (
);
const applyAggregation = React.useCallback(() => {
- const aggregationLookup = createAggregationLookup({
- apiRef,
- getAggregationPosition: props.getAggregationPosition,
- aggregationFunctions: props.aggregationFunctions,
- aggregationRowsScope: props.aggregationRowsScope,
- });
-
- apiRef.current.setState((state) => ({
- ...state,
- aggregation: { ...state.aggregation, lookup: aggregationLookup },
- }));
+ const { rulesOnLastRowHydration } = apiRef.current.caches.aggregation;
+
+ const aggregationRules = props.disableAggregation
+ ? {}
+ : getAggregationRules({
+ columnsLookup: gridColumnLookupSelector(apiRef),
+ aggregationModel: gridAggregationModelSelector(apiRef),
+ aggregationFunctions: props.aggregationFunctions,
+ });
+
+ if (
+ !areAggregationRulesEqual(rulesOnLastRowHydration, aggregationRules) ||
+ !apiRef.current.state.aggregation.lookup
+ ) {
+ const aggregationLookup = createAggregationLookup({
+ apiRef,
+ getAggregationPosition: props.getAggregationPosition,
+ aggregationFunctions: props.aggregationFunctions,
+ aggregationRowsScope: props.aggregationRowsScope,
+ });
+
+ apiRef.current.setState((state) => ({
+ ...state,
+ aggregation: { ...state.aggregation, lookup: aggregationLookup },
+ }));
+ }
}, [
apiRef,
- props.getAggregationPosition,
+ props.disableAggregation,
props.aggregationFunctions,
+ props.getAggregationPosition,
props.aggregationRowsScope,
]); This is definitely not an ultimate fix as it partially solves the problem of not re-computing the aggregation lookup on rows expansion change, but it causes another bug, i.e. now the grid is not responding properly to the aggregation function change. Another Dimension:On the codesandbox where the package for this PR is installed, I played around and I observed that the page was not anymore stuck on row expansion change, but when I tried to change an aggregation function, it blocked the UI thread for a similar amount of time: I think the reason is that our diffing algorithm is not smart enough to check, "ok, so out of 100 columns, the aggregation functions for column 1 has changed, let's go ahead and only re-compute the aggregation lookup for this column", rather it recomputes all the data once any column updates it's aggregation method. So maybe this direction could be a better one:
What do you think? |
packages/grid/x-data-grid-premium/src/DataGridPremium/useDataGridPremiumComponent.tsx
Outdated
Show resolved
Hide resolved
packages/grid/x-data-grid-premium/src/hooks/features/rowGrouping/gridRowGroupingUtils.ts
Show resolved
Hide resolved
@@ -17,6 +17,7 @@ import { | |||
createRowTree, |
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.
On the codesandbox where the package for this PR is installed, I played around and I observed that the page was not anymore stuck on row expansion change, but when I tried to change an aggregation function, it blocked the UI thread for a similar amount of time:
I think the reason is that our diffing algorithm is not smart enough to check, "ok, so out of 100 columns, the aggregation functions for column 1 has changed, let's go ahead and only re-compute the aggregation lookup for this column", rather it recomputes all the data once any column updates it's aggregation method.
That's a very good point, I'm sure we can optimize aggregation (especially when there's a lot of aggregated columns) 👍
visibleRows: visibleRowsState, | ||
}; | ||
}); | ||
apiRef.current.forceUpdate(); |
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.
Wouldn't be easier to understand if we updated the visibleRows
state also inside
const updateFilteredRows = React.useCallback(() => { |
filteredRowsSet
event? We can make an exception and have a single hook controlling two states. This apiRef.current.forceUpdate
call here but not in the filtering hook seems inconsistent. We don't forceUpdate
there but expect that one listener of filteredRowsSet
will call it. I had some problems in #9112 where, in one of the solutions I tried, I removed a forceUpdate
from a method that was doing nothing and one test started to fail. I think it would be better if we tried to rely less on event listeners and make the code less "connected". At the end we need:
- update both states when
applyFilters
is called - update only
visibleRows
if a row is expanded
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.
Good point, it will be easier to follow 👍
For the filteredRowsSet
event, it will still be used by the aggregation hook.
const getVisibleRows: GridStrategyProcessor<'visibleRows'> = (params) => { | ||
return { | ||
lookup: getVisibleRowsLookup(params), | ||
}; | ||
}; |
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.
Do we need to wrap in a return object?
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.
Not really. I added a wrapper when I extracted the state management to a separate hook, but now that it's coupled with the filter state - I have removed the wrapper and renamed visibleRows
to visibleRowsLookup
1aca18c
(#8671)
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.
A very nice optimization 🚀
Great work!
packages/grid/x-data-grid/src/hooks/features/filter/useGridFilter.tsx
Outdated
Show resolved
Hide resolved
packages/grid/x-data-grid/src/hooks/features/filter/useGridFilter.tsx
Outdated
Show resolved
Hide resolved
@@ -0,0 +1,12 @@ | |||
import type { GridRowId } from '../../../models/gridRows'; |
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.
Nit: Should this interface and gridVisibleRowsSelectors
be moved in close proximity to the rest of the code hooks/features/filter/gridFilterState
or models/gridVisibleRows
for better organization of files as it's being used in useGridFilter
?
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.
Yeah, I'll move everything back to the filtering files (it's where it was originally anyway, before the state split)
packages/grid/x-data-grid-pro/src/hooks/features/treeData/useGridTreeDataPreProcessors.tsx
Outdated
Show resolved
Hide resolved
|
||
visibleRowsLookup[node.id] = isPassingFiltering && areAncestorsExpanded; | ||
|
||
// TODO rows v6: Should we keep storing the visibility status of footer independently or rely on the group visibility in the selector ? |
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.
Do you have any opinions on this comment? Are footers sometimes hidden?
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, you can have aggregation footer for each row group - see https://mui.com/x/react-data-grid/aggregation/#AggregationGetAggregationPosition.tsx
When the group is filtered out, the aggregation footer should be hidden as well.
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.
IIUC, the visibility of the group is always equal to the visibility of the footer, right?
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.
Not exactly, you can have a visible row group, but if it's not expanded - the footer row shouldn't be visible:
mui-x/packages/grid/x-data-grid-pro/src/utils/tree/utils.ts
Lines 268 to 269 in 9aa0d40
visibleRowsLookup[node.footerId] = | |
isPassingFiltering && areAncestorsExpanded && !!node.childrenExpanded; |
row-group-footer.mov
/** | ||
* Visibility status for each row. | ||
* A row is visible if it is passing the filters AND if its parents are expanded. | ||
* If a row is not registered in this lookup, it is visible. | ||
*/ | ||
export interface GridVisibleRowsLookupState extends Record<GridRowId, boolean> {} |
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.
Just fyi, you may have seen in my filtering PR, but I was hoping to convert both the filteredRows & visibleRows from objects into Set
s as it improves performance for large datasets.
Minor note on the type, I would find type GridVisibleRowsLookupState = Record<GridRowId, boolean>
more semantically accurate. We're not building a new interface, we're defining what the shape of that state is, and it is a record.
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.
Just fyi, you may have seen in my filtering PR, but I was hoping to convert both the filteredRows & visibleRows from objects into Sets as it improves performance for large datasets.
Yes, but that would be a breaking change, right?
Unless we do something like this:
/**
* For backward compatibility only, in case if anyone is using `gridVisibleRowsLookupSetSelector`
* @deprecated Use `gridVisibleRowsLookupSetSelector` instead
*/
export const gridVisibleRowsLookupSelector = (state: GridStateCommunity) => setToObject(state.visibleRowsLookup);
// TODO v7: rename to `gridVisibleRowsLookupSelector`
export const gridVisibleRowsLookupSetSelector = (state: GridStateCommunity) => state.visibleRowsLookup;
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.
Minor note on the type, I would find type GridVisibleRowsLookupState = Record<GridRowId, boolean> more semantically accurate.
Updated 👍
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 for the change, that's what I'm planning to do for the selectors.
Fixes #8579
Before: https://codesandbox.io/s/friendly-gwen-w9ftyn?file=/demo.tsx
After: https://codesandbox.io/s/hardcore-mahavira-n88l5j?file=/demo.tsx
See #8579 (comment) for the issue explanation.
To avoid filtering rows on
rowExpansionChange
, I extractedvisibleRowsLookup
out of the filter state:Pros:
rowExpansionChange
Cons:
visibleRowsLookup
was updated along withstate.filter.filteredRowsLookup
in the same iteration overstate.rows.tree
. After this change, we iterate overstate.rows.tree
again to computestate.visibleRowsLookup
.TODO: