-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
[Data Grid] Avoid <GridRoot />
double-render pass on mount in SPA mode
#15648
base: master
Are you sure you want to change the base?
Conversation
Deploy preview: https://deploy-preview-15648--material-ui-x.netlify.app/ Updated pages: |
dd1a1e1
to
8da261b
Compare
<GridRoot />
double-render pass on mount in SPA mode
Side-benefit is also probably more robust tests if the Datagrid is mounted on the first pass. |
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.
Code LGTM, we can merge once the tests are passing.
I have no idea how to fix the last failing test. Either it's a symptom of column headers updated more times now or just a bad test. But I lack understanding why it was previously expected that the warning message will be output exactly twice. Edit: discovered some areas to improve around mounting / state initialization |
12fb3da
to
c8580d9
Compare
Also fixes an annoying flicker coming out of loading state. It's probably the root cause of a layout shift I once reported with Before: After: |
Tests should be passing now. 4 was the right amount of warnings for this approach, 1 extra pass (+1 warning for header/row each = 2+2 = 4) that doesn't even flush to the dom, so I think we're good there. Curiously, after this optimisation: mui-x/packages/x-data-grid/src/hooks/features/columns/useGridColumns.tsx Lines 400 to 412 in 5ca918f
This test started failing: mui-x/packages/x-data-grid-premium/src/tests/aggregation.DataGridPremium.test.tsx Lines 145 to 150 in 1a079e1
The only reason I can think of is that aggregations didn't explicitly trigger column updates, but somehow were piggybacking on If that's the case, then I suppose there was a bug whereby columns didn't clear when there was no totals row visible, as |
packages/x-data-grid/src/hooks/features/virtualization/useGridVirtualScroller.tsx
Outdated
Show resolved
Hide resolved
packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts
Outdated
Show resolved
Hide resolved
Thanks for the additional context, sounds like an interesting approach. I thought about alternative approaches as well, but after some quick measurements, I was more leaning towards the number of selectors not being an issue in and of itself – they're pretty performant. It's only an issue when the state updates a) with high frequency and b) we're rendering based on each state change / where every last ms counts. The only two areas I can think of are So, that leaves only |
}; | ||
} | ||
|
||
const scrollbarSizeCache = new WeakMap<Element, number>(); |
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 minor downside is that the data grid won't properly react to scrollbar size change (e.g. on Mac "Show scroll bars: Automatically => Always).
It's faster though, so let's keep it like this.
@@ -103,7 +102,9 @@ export const filterRowTreeFromTreeData = ( | |||
} | |||
} | |||
|
|||
filteredRowsLookup[node.id] = shouldPassFilters; | |||
if (!shouldPassFilters) { | |||
filteredRowsLookup[node.id] = false; |
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 spotted 2 places like this where we were still assigning true
values, so I narrowed the type in 3449782
(#15648)
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 catch, thanks. Fixed a few checks as well on that front, and tests are passing again for all cases.
Since streamlining state updates made Before: master-detail-before.mp4After: master-detail-after.mp4 |
@@ -67,4 +73,4 @@ export type GridFilteringMethodValue = Omit<GridFilterState, 'filterModel'>; | |||
* 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 type GridVisibleRowsLookupState = Record<GridRowId, boolean>; | |||
export type GridVisibleRowsLookupState = Record<GridRowId, false>; |
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.
Same change for visibleRowsLookup
as previously with filteredRowsLookup
.
@@ -20,7 +26,7 @@ export interface GridFilterState { | |||
* If a row is not registered in this lookup, it is filtered. | |||
* This is the equivalent of the `visibleRowsLookup` if all the groups were expanded. | |||
*/ | |||
filteredRowsLookup: Record<GridRowId, boolean>; | |||
filteredRowsLookup: Record<GridRowId, false>; |
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.
Since we don't store true
values now, does it make sense to simplify the type further? It could be slightly better performance wise, and more aligned DX wise, as the filteredRowsLookup
no longer stores the "filtered" rows, rather it stores "excluded" ones.
For consistency, a similar concept could be applied on the visible rows (invisibleRows: Set<GridRowId>
), column visibility model (hiddenColumns: Set<GridColDef['field']>
), etc.
filteredRowsLookup: Record<GridRowId, false>; | |
excludedRows: Set<GridRowId>; |
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.
Changing Record
to Set
/Map
won't necessarily be faster – see #9120 (comment)
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.
See #10068 (comment) and #10068 (comment). I think the change is good and has potential for improvement, but I didn't have the bandwidth to investigate it more.
@@ -20,7 +26,7 @@ export interface GridFilterState { | |||
* If a row is not registered in this lookup, it is filtered. |
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 a row is not registered in this lookup, it is filtered. | |
* All the rows are filtered except the ones registered in this lookup with `false` values. |
firstColumnIndex: undefined, | ||
lastColumnIndex: undefined, |
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 this value isn't used, can we use -1
as an empty value marker instead? It will avoid changing the shape of those objects.
const handleColumnHeaderDragStart = useEventCallback(() => { | ||
setDragging(true); | ||
}); | ||
|
||
const handleColumnHeaderDragEnd = useEventCallback(() => { | ||
setDragging(false); | ||
}); | ||
|
||
useGridApiEventHandler(apiRef, 'columnHeaderDragStart', handleColumnHeaderDragStart); | ||
useGridApiEventHandler(apiRef, 'columnHeaderDragEnd', handleColumnHeaderDragEnd); |
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.
useGridApiEventHandler
doesn't need stable functions (with useEventCallback
), it already replicates the same logic that useEventCallback
implements.
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.
This file should probably be under the features/dimensions
folder, to keep with existing conventions. You can add an export { ... } from '.../features/dimensions/dimensionSelectors'
where appropriate to expose these from @mui/x-data-grid/internals
.
export const gridDimensionsColumnsTotalWidthSelector = (state: GridStateCommunity) => | ||
state.dimensions.columnsTotalWidth; |
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.
You said there is a name conflict with another selector called gridColumnsTotalWidthSelector
. Are the two selectors returning the same value, calculated differently? If so, would it be appropriate to remove one of them?
Otherwise, could we rename the other one and keep the short name for this one?
renderContext?: | ||
| GridRenderContext | ||
| (Pick<GridRenderContext, 'firstRowIndex' | 'lastRowIndex'> & { | ||
firstColumnIndex: undefined; | ||
lastColumnIndex: undefined; | ||
}); |
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.
We could also use -1
for the type.
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.
LGTM beyond the small comments above.
Currently, GridRoot double-renders in all cases on mount to prevent SSR. In SPA-mode this is irrelevant, and can be avoided with the help of
useSyncExternalStore
, since there's no server snapshot in SPA-mode, the gird will be mounted directly. As a result, the rootRef becomes immediately available on first mount in SPAs, as one would expect.Adds a new dependency
use-sync-external-store
to make it backwards compatible with React 17. Charts and treeview have the same dependency.Changelog
Breaking changes
The
filteredRowsLookup
object of the filter state does not containtrue
values anymore. If the row is filtered out, the value isfalse
. Otherwise, the row id is not present in the object.This change only impacts you if you relied on
filteredRowsLookup
to get ids of filtered rows. In this case,usegridDataRowIdsSelector
selector to get row ids and checkfilteredRowsLookup
forfalse
values: