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

[TreeView] Do not re-render every Tree Item when the Rich Tree View re-renders (introduce selectors) #14210

Merged
merged 58 commits into from
Nov 15, 2024

Conversation

flaviendelangle
Copy link
Member

@flaviendelangle flaviendelangle commented Aug 14, 2024

Part of #14200

Work

Internal changes

  • Rename itemMetaMap => itemMetaLookup to make clear it is an object and not a Map (now that we do have map in the state)
  • Rename itemMap => itemModelLookup to make clear it is an object and not a Map (now that we do have map in the state) and to be more precise about what it contains
  • Rename itemOrderedChildrenIds => itemOrderedChildrenIdsLookup to make its purpose more clear
  • Rename itemChildrenIndexes => itemChildrenIndexesLookup to make its purpose more clear
  • Move disabledItemsFocusabflaviendelanglele to the state instead of passing it to the contextValue, it allows dropping instance.isItemNavigable in favor of a selector
  • Move the focus state in a focus key for consistency
  • Move the label editing state in a label key for consistency
  • Store the selectedItemsMap in the state to be usable in selectors
  • Store the expandedItemsMap in the state to be usable in selectors
  • Store the defaultFocusableItemId in the state to be usable in selectors

Introduce selectors

  • Replace the state with a store stored in a ref

    // Access
    -const itemsReordering = state.itemsReordering;
    +const itemsReordering = store.value.itemsReordering; // without selectors (should not do it);
    +const itemsReordering = selectorItemsReordering(store.value); // with selectors
    
    // Updates
    -setState((prevState) => ({ ...prevState, itemsReordering: null }));
    +store.update((prevState) => ({ ...prevState, itemsReordering: null }));
  • Create selectors for every state access

    The selectors are defined by each plugin in a useTreeViewXXX.selectors.ts file:

    // Each plugin has a root selector that returns its entire state.
    // This selector is not exported by the file
    const selectorTreeViewItemsState: TreeViewRootSelector<UseTreeViewItemsSignature> = (state) =>
    state.items;
    
    // The plugin then defines selectors using `createSelectors` 
    const selectorItemMetaLookup = createSelector(
      selectorTreeViewItemsState,
      (items) => items.itemMetaLookup,
    );
    
    // Those selectors can also accept additional arguments
    const selectorItemOrderedChildrenIds = createSelector(
      [selectorTreeViewItemsState, (_, itemId: string | null) => itemId],
      (itemsState, itemId) =>
        itemsState.itemOrderedChildrenIdsLookup[itemId ?? TREE_VIEW_ROOT_PARENT_ID] ?? EMPTY_CHILDREN,
    );
  • Replace every instance call in the render with selectors

    // For selectors without additional parameters
    const itemMetaLookup = useSelector(store, selectorItemMetaLookup);
    
    // For selectors with additional parameters
    -const isExpanded = instance.isItemExpanded(itemId);
    +const isExpanded = useSelector(store, selectorIsItemExpanded, itemId);
  • BREAKING: Add idAttribute to TreeItemProviderProps (to stop getting it from itemMeta, see [TreeView] Do not re-render every Tree Item when the Rich Tree View re-renders (introduce selectors) #14210 (comment) for context)

Add memoization

  • BREAKING: Add React.memo around the WrappedTreeItem component in RichTreeViewItems
  • Make sure every Tree Item do not re-render every time the Tree View re-renders (even when it has children)
  • Add tests

Docs

  • Update the doc examples to not use publicAPI in the render

Extracted PRs

Changelog

Breaking changes

  • The Tree Item component can no longer use publicAPI methods in the render because they are now memoized — Learn more.

@flaviendelangle flaviendelangle added the component: tree view TreeView, TreeItem. This is the name of the generic UI component, not the React module! label Aug 14, 2024
@flaviendelangle flaviendelangle self-assigned this Aug 14, 2024
@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Sep 2, 2024
Copy link

github-actions bot commented Sep 2, 2024

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Sep 6, 2024
@flaviendelangle flaviendelangle changed the title [tree view] Introduce selectors (does not work for now) [tree view] Introduce selectors Sep 13, 2024
@flaviendelangle flaviendelangle changed the title [tree view] Introduce selectors [TreeView] Introduce selectors Sep 13, 2024
@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Sep 19, 2024
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot added PR: out-of-date The pull request has merge conflicts and can't be merged and removed PR: out-of-date The pull request has merge conflicts and can't be merged labels Sep 19, 2024
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Sep 25, 2024
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot added PR: out-of-date The pull request has merge conflicts and can't be merged and removed PR: out-of-date The pull request has merge conflicts and can't be merged labels Sep 25, 2024
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot added PR: out-of-date The pull request has merge conflicts and can't be merged and removed PR: out-of-date The pull request has merge conflicts and can't be merged labels Sep 25, 2024
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Sep 26, 2024
@github-actions github-actions bot added PR: out-of-date The pull request has merge conflicts and can't be merged and removed PR: out-of-date The pull request has merge conflicts and can't be merged labels Oct 31, 2024
Copy link

github-actions bot commented Nov 7, 2024

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Nov 7, 2024
RichTreeViewItemsContext.displayName = 'RichTreeViewItemsProvider';
}

const WrappedTreeItem = React.memo(function WrappedTreeItem({
Copy link
Member Author

Choose a reason for hiding this comment

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

The main logic to be able to memoize the item is here.

Now, it's the WrappedTreeItem who accesses its meta data using a selector AND accesses its children using a selector.
That way, the props it passes to TreeItem (which has React.memo) only updates when there is an actual change.

We then use the RichTreeViewItemsContext context to allow nested items to render there own children.

Note that the whole memoization falls appart when an inline slotProps.item is provided 😬
I'll probably add a not on the doc, but I think it's not something we can avoid...

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Nov 7, 2024
Copy link

github-actions bot commented Nov 7, 2024

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Nov 7, 2024
Copy link
Contributor

@noraleonte noraleonte left a comment

Choose a reason for hiding this comment

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

First batch of review: The new dx looks good to me. It seems solid and I like that it fixes some internal hacks along the way 👌


const childrenNumber = publicAPI.getItemOrderedChildrenIds(props.itemId).length;
const selectFirstChildren = status.expanded
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the old example will be a lot easier to do with the lazy loading and getChildrenCount anyway 🤔

The example you created is fine for me 👌 There's just a bit of design inconsistency: the button makes the parent item taller and the text is misaligned vertically 🙈
image

}) => {
const editedItemRef = React.useRef(state.editedItemId);

const isItemBeingEditedRef = (itemId: TreeViewItemId) => editedItemRef.current === itemId;
Copy link
Contributor

Choose a reason for hiding this comment

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

That simplifies things a lot. I had a similar issue on the lazy loading when the fetching failed, I needed to undo the expansion of the item. My solution is a bit hacky, but this should help 👍

return typeof params.isItemEditable === 'function'
? params.isItemEditable(item)
: Boolean(params.isItemEditable);
store.update((prevState) => ({ ...prevState, label: { editedItemId } }));
Copy link
Contributor

Choose a reason for hiding this comment

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

Love how it simplified this part too

Copy link
Contributor

@noraleonte noraleonte left a comment

Choose a reason for hiding this comment

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

I went through the docs and I don't see any weird/buggy behavior 💪 Left some super small nitpicks, but other than that, everything looks good to me 👌 Congrats on this huge effort, it's super valuable 🚀

@@ -34,9 +41,21 @@ const CustomTreeItem = React.forwardRef(function CustomTreeItem(
props: TreeItemProps,
ref: React.Ref<HTMLLIElement>,
) {
const { publicAPI } = useTreeItem(props);
const { publicAPI, status } = useTreeItem(props);
Copy link
Contributor

Choose a reason for hiding this comment

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

unrelated to this PR, but I just realized we can do the same thing with useTreeItemUtils too 🤔 Could become confusing for user

Copy link
Member Author

Choose a reason for hiding this comment

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

True, I think it will be solved when we migrate the Tree Item to the Base UI DX since useTreeItem will go away.

@flaviendelangle flaviendelangle merged commit 17486e9 into mui:master Nov 15, 2024
16 checks passed
@flaviendelangle flaviendelangle deleted the selector-tree-view branch November 15, 2024 14:00

```ts
function CustomTreeItem(props) {
const { publicAPI } = useTreeItemUtils();
Copy link
Member

@oliviertassinari oliviertassinari Nov 23, 2024

Choose a reason for hiding this comment

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

This looks like a typo, shouldn't it be

Suggested change
const { publicAPI } = useTreeItemUtils();
const { publicApi } = useTreeItemUtils();

per the rest of the API conversion? We could add this to https://mui.com/material-ui/guides/api/.

Off-topic: Seeing "public" in the name feels a bit strange, it feels a bit like a tautology: if it's documented, it's public.

Copy link
Member Author

Choose a reason for hiding this comment

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

Both remark are totally valid.
It's publicAPI ( / publicApi) internally, but when exposed it should probably be api

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change component: tree view TreeView, TreeItem. This is the name of the generic UI component, not the React module! performance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants