Skip to content
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] Avoid <GridRoot /> double-render pass on mount in SPA mode #15648

Merged
merged 151 commits into from
Feb 5, 2025

Conversation

lauri865
Copy link
Contributor

@lauri865 lauri865 commented Nov 28, 2024

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 contain true values anymore. If the row is filtered out, the value is false. 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,use gridDataRowIdsSelector selector to get row ids and check filteredRowsLookup for false values:

     const filteredRowsLookup = gridFilteredRowsLookupSelector(apiRef);
    -const filteredRowIds = Object.keys(filteredRowsLookup).filter((rowId) => filteredRowsLookup[rowId] === true);
    +const rowIds = gridDataRowIdsSelector(apiRef);
    +const filteredRowIds = rowIds.filter((rowId) => filteredRowsLookup[rowId] !== false);
  • The visibleRowsLookup state does not contain true values anymore. If the row is not visible, the value is false. Otherwise, the row id is not present in the object:

     const visibleRowsLookup = gridVisibleRowsLookupSelector(apiRef);
    -const isRowVisible = visibleRowsLookup[rowId] === true;
    +const isRowVisible = visibleRowsLookup[rowId] !== false;

@mui-bot
Copy link

mui-bot commented Nov 28, 2024

@lauri865 lauri865 force-pushed the avoid-double-render-pass-in-spas branch from dd1a1e1 to 8da261b Compare November 28, 2024 13:53
@flaviendelangle flaviendelangle added the component: data grid This is the name of the generic UI component, not the React module! label Nov 28, 2024
@flaviendelangle flaviendelangle changed the title [Data Grid] Avoid GridRoot double-render pass on mount in SPA mode [Data Grid] Avoid <GridRoot /> double-render pass on mount in SPA mode Nov 28, 2024
@lauri865
Copy link
Contributor Author

Side-benefit is also probably more robust tests if the Datagrid is mounted on the first pass.

Copy link
Contributor

@romgrk romgrk left a 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.

@lauri865
Copy link
Contributor Author

lauri865 commented Dec 4, 2024

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

@lauri865 lauri865 force-pushed the avoid-double-render-pass-in-spas branch from 12fb3da to c8580d9 Compare December 4, 2024 12:39
@lauri865
Copy link
Contributor Author

lauri865 commented Dec 4, 2024

Also fixes an annoying flicker coming out of loading state. It's probably the root cause of a layout shift I once reported with autoHeight grid coming out of loading state.

Before:
https://github.com/user-attachments/assets/1a68b2de-f2fd-4ef4-a93d-215d044e35b7

After:
https://github.com/user-attachments/assets/cfe55012-092a-429d-a79d-75e243124d10

@lauri865
Copy link
Contributor Author

lauri865 commented Dec 4, 2024

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:

const hasFlexColumns = gridVisibleColumnDefinitionsSelector(apiRef).some(
(col) => col.flex && col.flex > 0,
);
if (!hasFlexColumns) {
return;
}
setGridColumnsState(
hydrateColumnsWidth(
gridColumnsStateSelector(apiRef.current.state),
apiRef.current.getRootDimensions(),
),
);

This test started failing:

it('should correctly restore the column when changing from aggregated to non-aggregated', () => {
const { setProps } = render(<Test aggregationModel={{ id: 'max' }} />);
expect(getColumnHeaderCell(0, 0).textContent).to.equal('idmax');
setProps({ aggregationModel: {} });
expect(getColumnHeaderCell(0, 0).textContent).to.equal('id');
});

The only reason I can think of is that aggregations didn't explicitly trigger column updates, but somehow were piggybacking on viewportInnerSizeChange event for taking care of it. Adding an explicit updateColumns call fixed it. Is there any other explanation to it?

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 viewPortInnerSize would only change if there was a pinned row.

@@ -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>;
Copy link
Member

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.

packages/x-data-grid/src/hooks/utils/useGridSelector.ts Outdated Show resolved Hide resolved
@@ -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>;
Copy link
Member

@MBilalShafi MBilalShafi Feb 4, 2025

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.

Suggested change
filteredRowsLookup: Record<GridRowId, false>;
excludedRows: Set<GridRowId>;

Copy link
Member

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)

Copy link
Contributor

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.

@cherniavskii cherniavskii requested a review from romgrk February 4, 2025 15:15
Comment on lines 5 to 6
export const gridDimensionsColumnsTotalWidthSelector = (state: GridStateCommunity) =>
state.dimensions.columnsTotalWidth;
Copy link
Contributor

@romgrk romgrk Feb 4, 2025

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the two selectors returning the same value, calculated differently? If so, would it be appropriate to remove one of them?

No, the original gridColumnsTotalWidthSelector calculates the value, and that value change triggers the dimensions state update and gets stored in that state.
AFAIU @lauri865 used gridDimensionsColumnsTotalWidthSelector instead because it guarantees that the dimensions state is up to date.

I've moved the original selector to useGridDimensions, it's only used internally now.
I've also renamed gridDimensionsColumnsTotalWidthSelector to gridColumnsTotalWidthSelector and used createSelector to keep the signature of the original selector.

@lauri865 Does this make sense to you?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. I believe your change makes sense, as nothing else probably depends on the calculated size other than useGridDimensions, it makes sense to localise it.

Copy link
Contributor

@romgrk romgrk left a 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.

@cherniavskii cherniavskii merged commit ad55b60 into mui:master Feb 5, 2025
18 checks passed
Copy link

github-actions bot commented Feb 5, 2025

Cherry-pick PRs will be created targeting branches: v7.x

@cherniavskii cherniavskii changed the title [Data Grid] Avoid <GridRoot /> double-render pass on mount in SPA mode [DataGrid] Avoid <GridRoot /> double-render pass on mount in SPA mode Feb 5, 2025
cherniavskii added a commit to cherniavskii/mui-x that referenced this pull request Feb 5, 2025
(dimensions) => dimensions.columnsTotalWidth,
);

export const gridRowHeightSelector = (state: GridStateCommunity) => state.dimensions.rowHeight;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lauri865
any particular reason why all these selectors are not using gridDimensionsSelector as an input?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would it be solving to wrap them in another function call? Just went with my gut feel tbh - simple selectors, simpler to write plainly. Localized to a single file, enforced by typescript.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am asking because in this PR I have to replace all params with apiRef, so each one of these will have apiRef.current.state.dimensions.X repeated. Having just dimensions will be much easier to read.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They should probably be transformed to something like const xxxSelector = createRootSelector(state => state.dimensions.xxx) to keep them readable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: data grid This is the name of the generic UI component, not the React module! needs cherry-pick The PR should be cherry-picked to master after merge performance v7.x
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[data grid] performance regression on resizing on v7
7 participants