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
Merged
Changes from 1 commit
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
f0c8ac1
[TreeView] Introduce selectors
flaviendelangle Oct 23, 2024
a47c95e
Work
flaviendelangle Oct 23, 2024
e8a385c
Work
flaviendelangle Oct 23, 2024
0518706
Fix
flaviendelangle Oct 23, 2024
9ea8ab2
Fix tests
flaviendelangle Oct 23, 2024
2abe440
Merge
flaviendelangle Oct 23, 2024
9e302d2
Work for object children
flaviendelangle Oct 23, 2024
2307a65
Work
flaviendelangle Oct 24, 2024
479274f
Work
flaviendelangle Oct 24, 2024
eb00e1e
Work
flaviendelangle Oct 24, 2024
e9bbb6d
Work
flaviendelangle Oct 24, 2024
4b6ae73
Work
flaviendelangle Oct 24, 2024
99f3f07
Work
flaviendelangle Oct 24, 2024
a70d3dd
Work
flaviendelangle Oct 24, 2024
87d1992
Fix doc examples
flaviendelangle Oct 24, 2024
3cdc8f2
Work
flaviendelangle Oct 24, 2024
1181d38
Fix
flaviendelangle Oct 24, 2024
79228ef
Fix
flaviendelangle Oct 24, 2024
4cef1db
Merge branch 'master' into selector-tree-view
flaviendelangle Oct 24, 2024
66fde8c
Fix
flaviendelangle Oct 24, 2024
4987b9a
Fix
flaviendelangle Oct 24, 2024
451bac2
Fix
flaviendelangle Oct 24, 2024
9302614
Merge branch 'master' into selector-tree-view
flaviendelangle Oct 25, 2024
3b63a9d
Remove forceUpdate
flaviendelangle Oct 25, 2024
01879e5
Fix
flaviendelangle Oct 25, 2024
f834dca
Work
flaviendelangle Oct 25, 2024
efae98d
Add tests
flaviendelangle Oct 25, 2024
ab57913
Work
flaviendelangle Oct 25, 2024
d9f5dc9
Work
flaviendelangle Oct 25, 2024
7492a5e
Add JSDoc and improve DX
flaviendelangle Oct 25, 2024
da44f3a
Work
flaviendelangle Oct 25, 2024
315c107
Regen api
flaviendelangle Oct 25, 2024
f3d1fda
Fix
flaviendelangle Oct 25, 2024
59518ea
Fix
flaviendelangle Oct 25, 2024
96e4287
Fix
flaviendelangle Oct 25, 2024
909ed67
Merge branch 'master' into selector-tree-view
flaviendelangle Oct 28, 2024
3412fea
Merge branch 'master' into selector-tree-view
flaviendelangle Oct 29, 2024
41967f2
Add tests
flaviendelangle Oct 29, 2024
ad27780
Fix
flaviendelangle Oct 29, 2024
bf00041
Fix
flaviendelangle Oct 29, 2024
bb27ecb
Merge branch 'master' into selector-tree-view
flaviendelangle Oct 29, 2024
86662f6
Fix
flaviendelangle Oct 29, 2024
608757e
Fix
flaviendelangle Oct 29, 2024
095c407
Merge
flaviendelangle Oct 31, 2024
c0871e3
Merge
flaviendelangle Nov 7, 2024
fb0c67d
Merge
flaviendelangle Nov 7, 2024
2e8eec2
Merge branch 'master' into selector-tree-view
flaviendelangle Nov 8, 2024
93a8d0f
Merge branch 'master' into selector-tree-view
flaviendelangle Nov 12, 2024
43d7d3f
Review: Nora
flaviendelangle Nov 12, 2024
21b820c
Merge branch 'master' into selector-tree-view
flaviendelangle Nov 14, 2024
aaa6c7d
Add migration guide
flaviendelangle Nov 14, 2024
5172348
Merge branch 'master' into selector-tree-view
flaviendelangle Nov 14, 2024
d10fcc9
Merge branch 'master' into selector-tree-view
flaviendelangle Nov 14, 2024
5933ee6
Merge branch 'master' into selector-tree-view
flaviendelangle Nov 14, 2024
7fdd27d
Empty
flaviendelangle Nov 14, 2024
4c37ec2
Merge branch 'master' into selector-tree-view
flaviendelangle Nov 14, 2024
4d125f6
Merge branch 'master' into selector-tree-view
flaviendelangle Nov 15, 2024
fa353bd
Review: Nora
flaviendelangle Nov 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Work
flaviendelangle committed Oct 23, 2024

Verified

This commit was signed with the committer’s verified signature.
flaviendelangle Flavien DELANGLE
commit a47c95e2ee965b31cb27ef92f7f5e1720e70a31f
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as React from 'react';
import { TreeViewPlugin } from '@mui/x-tree-view/internals';
import {
TreeViewPlugin,
selectorItemIndex,
selectorItemMeta,
selectorItemOrderedChildrenIds,
} from '@mui/x-tree-view/internals';
import { warnOnce } from '@mui/x-internals/warning';
import { TreeViewItemsReorderingAction } from '@mui/x-tree-view/models';
import {
@@ -49,10 +54,10 @@ export const useTreeViewItemsReordering: TreeViewPlugin<UseTreeViewItemsReorderi
}

const canMoveItemToNewPosition = params.canMoveItemToNewPosition;
const targetItemMeta = instance.getItemMeta(itemId);
const targetItemIndex = instance.getItemIndex(targetItemMeta.id);
const draggedItemMeta = instance.getItemMeta(itemsReordering.draggedItemId);
const draggedItemIndex = instance.getItemIndex(draggedItemMeta.id);
const targetItemMeta = selectorItemMeta(store.value, itemId);
const targetItemIndex = selectorItemIndex(store.value, targetItemMeta.id);
const draggedItemMeta = selectorItemMeta(store.value, itemsReordering.draggedItemId);
const draggedItemIndex = selectorItemIndex(store.value, draggedItemMeta.id);

const oldPosition: TreeViewItemReorderPosition = {
parentId: draggedItemMeta.parentId,
@@ -108,7 +113,7 @@ export const useTreeViewItemsReordering: TreeViewPlugin<UseTreeViewItemsReorderi
? null
: {
parentId: targetItemMeta.parentId,
index: instance.getItemOrderedChildrenIds(targetItemMeta.parentId).length,
index: selectorItemOrderedChildrenIds(store.value, targetItemMeta.parentId).length,
},
};

@@ -122,7 +127,7 @@ export const useTreeViewItemsReordering: TreeViewPlugin<UseTreeViewItemsReorderi

return validActions;
},
[instance, store, params.canMoveItemToNewPosition],
[store, params.canMoveItemToNewPosition],
);

const startDraggingItem = React.useCallback(
@@ -156,11 +161,11 @@ export const useTreeViewItemsReordering: TreeViewPlugin<UseTreeViewItemsReorderi
return;
}

const draggedItemMeta = instance.getItemMeta(itemsReordering.draggedItemId);
const draggedItemMeta = selectorItemMeta(store.value, itemsReordering.draggedItemId);

const oldPosition: TreeViewItemReorderPosition = {
parentId: draggedItemMeta.parentId,
index: instance.getItemIndex(draggedItemMeta.id),
index: selectorItemIndex(store.value, draggedItemMeta.id),
};

const newPosition = itemsReordering.newPosition;
@@ -183,7 +188,7 @@ export const useTreeViewItemsReordering: TreeViewPlugin<UseTreeViewItemsReorderi
oldPosition,
});
},
[store, instance, params.onItemPositionChange],
[store, params.onItemPositionChange],
);

const setDragTargetItem = React.useCallback<
9 changes: 6 additions & 3 deletions packages/x-tree-view/src/TreeItem/useTreeItemState.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,10 @@ import { UseTreeViewItemsSignature } from '../internals/plugins/useTreeViewItems
import { UseTreeViewLabelSignature, useTreeViewLabel } from '../internals/plugins/useTreeViewLabel';
import { hasPlugin } from '../internals/utils/plugins';
import { useSelector } from '../internals/hooks/useSelector';
import { selectorIsItemExpanded } from '../internals/plugins/useTreeViewExpansion/useTreeViewExpansion.selectors';
import {
selectorIsItemExpandable,
selectorIsItemExpanded,
} from '../internals/plugins/useTreeViewExpansion/useTreeViewExpansion.selectors';
import { selectorIsItemFocused } from '../internals/plugins/useTreeViewFocus/useTreeViewFocus.selectors';
import { selectorIsItemDisabled } from '../internals/plugins/useTreeViewItems/useTreeViewItems.selectors';
import { selectorIsItemSelected } from '../internals/plugins/useTreeViewSelection/useTreeViewSelection.selectors';
@@ -37,7 +40,7 @@ export function useTreeItemState(itemId: string) {
label,
} = useTreeViewContext<UseTreeItemStateMinimalPlugins, UseTreeItemStateOptionalPlugins>();

const expandable = instance.isItemExpandable(itemId);
const expandable = useSelector(store, selectorIsItemExpandable, itemId);
const isExpanded = useSelector(store, selectorIsItemExpanded, itemId);
const isFocused = useSelector(store, selectorIsItemFocused, itemId);
const isSelected = useSelector(store, selectorIsItemSelected, itemId);
@@ -60,7 +63,7 @@ export function useTreeItemState(itemId: string) {
const multiple = multiSelect && (event.shiftKey || event.ctrlKey || event.metaKey);

// If already expanded and trying to toggle selection don't close
if (expandable && !(multiple && instance.isItemExpanded(itemId))) {
if (expandable && !(multiple && selectorIsItemExpanded(store.value, itemId))) {
instance.toggleItemExpansion(event, itemId);
}
}
Original file line number Diff line number Diff line change
@@ -119,7 +119,7 @@ export const useTreeItem2Utils = <
const multiple = multiSelect && (event.shiftKey || event.ctrlKey || event.metaKey);

// If already expanded and trying to toggle selection don't close
if (status.expandable && !(multiple && instance.isItemExpanded(itemId))) {
if (status.expandable && !(multiple && selectorIsItemExpanded(store.value, itemId))) {
instance.toggleItemExpansion(event, itemId);
}
};
3 changes: 3 additions & 0 deletions packages/x-tree-view/src/internals/index.ts
Original file line number Diff line number Diff line change
@@ -51,6 +51,9 @@ export {
buildSiblingIndexes,
TREE_VIEW_ROOT_PARENT_ID,
selectorItemMetaMap,
selectorItemMeta,
selectorItemIndex,
selectorItemOrderedChildrenIds,
} from './plugins/useTreeViewItems';
export type {
UseTreeViewItemsSignature,
Original file line number Diff line number Diff line change
@@ -4,3 +4,4 @@ export type {
UseTreeViewExpansionParameters,
UseTreeViewExpansionDefaultizedParameters,
} from './useTreeViewExpansion.types';
export { selectorIsItemExpandable, selectorIsItemExpanded } from './useTreeViewExpansion.selectors';
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createSelector, TreeViewRootSelector } from '../../utils/selectors';
import { selectorItemMeta } from '../useTreeViewItems';
import { UseTreeViewExpansionSignature } from './useTreeViewExpansion.types';

const selectorExpansion: TreeViewRootSelector<UseTreeViewExpansionSignature> = (state) =>
@@ -13,3 +14,8 @@ export const selectorIsItemExpanded = createSelector(
[selectorExpandedItemsMap, (_, itemId: string) => itemId],
(expandedItemsMap, itemId) => expandedItemsMap.has(itemId),
);

export const selectorIsItemExpandable = createSelector(
[selectorItemMeta],
(itemMeta) => itemMeta.expandable,
);
Original file line number Diff line number Diff line change
@@ -4,8 +4,9 @@ import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
import { TreeViewPlugin } from '../../models';
import { UseTreeViewExpansionSignature } from './useTreeViewExpansion.types';
import { TreeViewItemId } from '../../../models';
import { selectorExpandedItemsMap } from './useTreeViewExpansion.selectors';
import { selectorIsItemExpandable, selectorIsItemExpanded } from './useTreeViewExpansion.selectors';
import { getExpandedItemsMap } from './useTreeViewExpansion.utils';
import { selectorItemMeta, selectorItemOrderedChildrenIds } from '../useTreeViewItems';

export const useTreeViewExpansion: TreeViewPlugin<UseTreeViewExpansionSignature> = ({
instance,
@@ -30,26 +31,16 @@ export const useTreeViewExpansion: TreeViewPlugin<UseTreeViewExpansionSignature>
models.expandedItems.setControlledValue(value);
};

const isItemExpanded = React.useCallback(
(itemId: string) => selectorExpandedItemsMap(store.value).has(itemId),
[store],
);

const isItemExpandable = React.useCallback(
(itemId: string) => !!instance.getItemMeta(itemId)?.expandable,
[instance],
);

const toggleItemExpansion = useEventCallback(
(event: React.SyntheticEvent, itemId: TreeViewItemId) => {
const isExpandedBefore = instance.isItemExpanded(itemId);
const isExpandedBefore = selectorIsItemExpanded(store.value, itemId);
instance.setItemExpansion(event, itemId, !isExpandedBefore);
},
);

const setItemExpansion = useEventCallback(
(event: React.SyntheticEvent, itemId: TreeViewItemId, isExpanded: boolean) => {
const isExpandedBefore = instance.isItemExpanded(itemId);
const isExpandedBefore = selectorIsItemExpanded(store.value, itemId);
if (isExpandedBefore === isExpanded) {
return;
}
@@ -70,11 +61,12 @@ export const useTreeViewExpansion: TreeViewPlugin<UseTreeViewExpansionSignature>
);

const expandAllSiblings = (event: React.KeyboardEvent, itemId: TreeViewItemId) => {
const itemMeta = instance.getItemMeta(itemId);
const siblings = instance.getItemOrderedChildrenIds(itemMeta.parentId);
const itemMeta = selectorItemMeta(store.value, itemId);
const siblings = selectorItemOrderedChildrenIds(store.value, itemMeta.parentId);

const diff = siblings.filter(
(child) => instance.isItemExpandable(child) && !instance.isItemExpanded(child),
(child) =>
selectorIsItemExpandable(store.value, child) && !selectorIsItemExpanded(store.value, child),
);

const newExpanded = models.expandedItems.value.concat(diff);
@@ -116,8 +108,6 @@ export const useTreeViewExpansion: TreeViewPlugin<UseTreeViewExpansionSignature>
setItemExpansion,
},
instance: {
isItemExpanded,
isItemExpandable,
setItemExpansion,
toggleItemExpansion,
expandAllSiblings,
Original file line number Diff line number Diff line change
@@ -15,20 +15,6 @@ export interface UseTreeViewExpansionPublicAPI {
}

export interface UseTreeViewExpansionInstance extends UseTreeViewExpansionPublicAPI {
/**
* Check if an item is expanded.
* @param {TreeViewItemId} itemId The id of the item to check.
* @returns {boolean} `true` if the item is expanded, `false` otherwise.
*/
isItemExpanded: (itemId: TreeViewItemId) => boolean;
/**
* Check if an item is expandable.
* Currently, an item is expandable if it has children.
* In the future, the user should be able to flag an item as expandable even if it has no loaded children to support children lazy loading.
* @param {TreeViewItemId} itemId The id of the item to check.
* @returns {boolean} `true` if the item can be expanded, `false` otherwise.
*/
isItemExpandable: (itemId: TreeViewItemId) => boolean;
/**
* Toggle the current expansion of an item.
* If it is expanded, it will be collapsed, and vice versa.
Original file line number Diff line number Diff line change
@@ -12,6 +12,8 @@ import {
selectorDefaultFocusableItemId,
selectorFocusedItemId,
} from './useTreeViewFocus.selectors';
import { selectorIsItemExpanded } from '../useTreeViewExpansion';
import { selectorItemMeta } from '../useTreeViewItems';

const useDefaultFocusableItemId = (
instance: TreeViewUsedInstance<UseTreeViewFocusSignature>,
@@ -24,12 +26,15 @@ const useDefaultFocusableItemId = (
return false;
}

const itemMeta = instance.getItemMeta(itemId);
return itemMeta && (itemMeta.parentId == null || instance.isItemExpanded(itemMeta.parentId));
const itemMeta = selectorItemMeta(store.value, itemId);
return (
itemMeta &&
(itemMeta.parentId == null || selectorIsItemExpanded(store.value, itemMeta.parentId))
);
});

if (defaultFocusableItemId == null) {
defaultFocusableItemId = getFirstNavigableItem(instance) ?? null;
defaultFocusableItemId = getFirstNavigableItem(instance, store) ?? null;
}

store.update((prevState) => {
@@ -67,8 +72,11 @@ export const useTreeViewFocus: TreeViewPlugin<UseTreeViewFocusSignature> = ({
});

const isItemVisible = (itemId: string) => {
const itemMeta = instance.getItemMeta(itemId);
return itemMeta && (itemMeta.parentId == null || instance.isItemExpanded(itemMeta.parentId));
const itemMeta = selectorItemMeta(store.value, itemId);
return (
itemMeta &&
(itemMeta.parentId == null || selectorIsItemExpanded(store.value, itemMeta.parentId))
);
};

const innerFocusItem = (event: React.SyntheticEvent | null, itemId: string) => {
@@ -97,7 +105,7 @@ export const useTreeViewFocus: TreeViewPlugin<UseTreeViewFocusSignature> = ({
return;
}

const itemMeta = instance.getItemMeta(focusedItemId);
const itemMeta = selectorItemMeta(store.value, focusedItemId);
if (itemMeta) {
const itemElement = instance.getItemDOMElement(focusedItemId);
if (itemElement) {
Original file line number Diff line number Diff line change
@@ -10,5 +10,6 @@ export { buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID } from './useTreeViewItem
export {
selectorItemMetaMap,
selectorItemMeta,
selectorItemIndex,
selectorItemOrderedChildrenIds,
} from './useTreeViewItems.selectors';
Original file line number Diff line number Diff line change
@@ -32,8 +32,8 @@ export const selectorItemChildrenIndexes = createSelector(
export const selectorItemMap = createSelector(selectorTreeViewItemsState, (items) => items.itemMap);

export const selectorItemMeta = createSelector(
[selectorItemMetaMap, (_, itemId: string) => itemId],
(itemMetaMap, itemId) => itemMetaMap[itemId],
[selectorItemMetaMap, (_, itemId: string | null) => itemId],
(itemMetaMap, itemId) => itemMetaMap[itemId ?? TREE_VIEW_ROOT_PARENT_ID],
);

export const selectorIsItemDisabled = createSelector(
@@ -64,3 +64,11 @@ export const selectorIsItemDisabled = createSelector(
return false;
},
);

/**
* Get the index of a given item in its parent's children list.
*/
export const selectorItemIndex = createSelector(
[selectorItemMeta, selectorItemChildrenIndexes],
(itemMeta, indexes) => indexes[itemMeta.parentId ?? TREE_VIEW_ROOT_PARENT_ID][itemMeta.id],
);
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ import { TreeViewBaseItem, TreeViewItemId } from '../../../models';
import { buildSiblingIndexes, TREE_VIEW_ROOT_PARENT_ID } from './useTreeViewItems.utils';
import { TreeViewItemDepthContext } from '../../TreeViewItemDepthContext';
import {
selectorIsItemDisabled,
selectorItemChildrenIndexes,
selectorItemMap,
selectorItemMeta,
@@ -140,50 +141,13 @@ export const useTreeViewItems: TreeViewPlugin<UseTreeViewItemsSignature> = ({
return instance.getItemOrderedChildrenIds(null).map(getItemFromItemId);
}, [instance, store]);

const isItemDisabled = React.useCallback(
(itemId: string | null): itemId is string => {
if (itemId == null) {
return false;
}

let itemMeta = instance.getItemMeta(itemId);

// This can be called before the item has been added to the item map.
if (!itemMeta) {
return false;
}

if (itemMeta.disabled) {
return true;
}

while (itemMeta.parentId != null) {
itemMeta = instance.getItemMeta(itemMeta.parentId);
if (itemMeta.disabled) {
return true;
}
}

return false;
},
[instance],
);

const getItemIndex = React.useCallback(
(itemId: string) => {
const parentId = instance.getItemMeta(itemId).parentId ?? TREE_VIEW_ROOT_PARENT_ID;
return selectorItemChildrenIndexes(store.value)[parentId][itemId];
},
[instance, store],
);

const getItemOrderedChildrenIds = React.useCallback(
(itemId: string | null) => selectorItemOrderedChildrenIds(store.value, itemId),
[store],
);

const getItemDOMElement = (itemId: string) => {
const itemMeta = instance.getItemMeta(itemId);
const itemMeta = selectorItemMeta(store.value, itemId);
if (itemMeta == null) {
return null;
}
@@ -200,7 +164,7 @@ export const useTreeViewItems: TreeViewPlugin<UseTreeViewItemsSignature> = ({
if (params.disabledItemsFocusable) {
return true;
}
return !instance.isItemDisabled(itemId);
return !selectorIsItemDisabled(store.value, itemId);
};

const areItemUpdatesPreventedRef = React.useRef(false);
@@ -235,11 +199,11 @@ export const useTreeViewItems: TreeViewPlugin<UseTreeViewItemsSignature> = ({

const getItemsToRender = () => {
const getPropsFromItemId = (id: TreeViewItemId): TreeViewItemToRenderProps => {
const item = instance.getItemMeta(id);
const itemMeta = selectorItemMeta(store.value, id);
return {
label: item.label!,
itemId: item.id,
id: item.idAttribute,
label: itemMeta.label!,
itemId: itemMeta.id,
id: itemMeta.idAttribute,
children: instance.getItemOrderedChildrenIds(id).map(getPropsFromItemId),
};
};
@@ -288,7 +252,6 @@ export const useTreeViewItems: TreeViewPlugin<UseTreeViewItemsSignature> = ({
getItemIndex,
getItemDOMElement,
getItemOrderedChildrenIds,
isItemDisabled,
isItemNavigable,
preventItemUpdates,
areItemUpdatesPrevented,
Loading