Skip to content

Commit e717ee9

Browse files
authored
fix: Speed up tree collection builder flattening when expanding keys (#8774)
* fix: Speed up tree collection builder * add story for verification
1 parent a9a451c commit e717ee9

File tree

2 files changed

+81
-2
lines changed

2 files changed

+81
-2
lines changed

packages/react-aria-components/src/Tree.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,7 @@ function flattenTree<T>(collection: TreeCollection<T>, opts: TreeGridCollectionO
818818
let flattenedRows: Node<T>[] = [];
819819
// Need to count the items here because BaseCollection will return the full item count regardless if items are hidden via collapsed rows
820820
let itemCount = 0;
821+
let parentLookup: Map<Key, boolean> = new Map();
821822

822823
let visitNode = (node: Node<T>) => {
823824
if (node.type === 'item' || node.type === 'loader') {
@@ -844,12 +845,13 @@ function flattenTree<T>(collection: TreeCollection<T>, opts: TreeGridCollectionO
844845

845846
// Grab the modified node from the key map so our flattened list and modified key map point to the same nodes
846847
let modifiedNode = keyMap.get(node.key) || node;
847-
if (modifiedNode.level === 0 || (modifiedNode.parentKey != null && expandedKeys.has(modifiedNode.parentKey) && flattenedRows.find(row => row.key === modifiedNode.parentKey))) {
848+
if (modifiedNode.level === 0 || (modifiedNode.parentKey != null && expandedKeys.has(modifiedNode.parentKey) && parentLookup.get(modifiedNode.parentKey))) {
848849
if (modifiedNode.type === 'item') {
849850
itemCount++;
850851
}
851852

852853
flattenedRows.push(modifiedNode);
854+
parentLookup.set(modifiedNode.key, true);
853855
}
854856
} else if (node.type !== null) {
855857
keyMap.set(node.key, node as CollectionNode<T>);

packages/react-aria-components/stories/Tree.stories.tsx

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ let defaultExpandedKeys = new Set(['projects', 'project-2', 'project-5', 'report
426426

427427
const TreeExampleDynamicRender = <T extends object>(args: TreeProps<T>): JSX.Element => {
428428
let treeData = useTreeData<any>({
429-
initialItems: rows,
429+
initialItems: args.items as any ?? rows,
430430
getKey: item => item.id,
431431
getChildren: item => item.childItems
432432
});
@@ -1196,3 +1196,80 @@ export const TreeWithDragAndDropVirtualized = {
11961196
render: TreeDragAndDropVirtualizedRender,
11971197
name: 'Tree with drag and drop (virtualized)'
11981198
};
1199+
1200+
1201+
interface ITreeItem {
1202+
id: string,
1203+
name: string,
1204+
childItems?: Iterable<ITreeItem>
1205+
}
1206+
1207+
let totalItems = 0;
1208+
let itemKeys = new Set<any>();
1209+
/**
1210+
* Generates a tree data structure with 10 items per level and 6 levels deep.
1211+
* @returns Array of tree items with the specified structure.
1212+
*/
1213+
function generateTreeData(): Array<ITreeItem> {
1214+
/**
1215+
* Recursively generates tree items for a given level.
1216+
* @param level - Current depth level (1-6).
1217+
* @param parentId - Parent item ID for generating unique child IDs.
1218+
* @returns Array of tree items for this level.
1219+
*/
1220+
function generateLevel(level: number, parentId: string = ''): Array<ITreeItem> {
1221+
const items: ITreeItem[] = [];
1222+
const itemsPerLevel = 7;
1223+
1224+
for (let i = 1; i < itemsPerLevel; i++) {
1225+
const itemId = parentId ? `${parentId}-${i}` : `level-${level}-item-${i}`;
1226+
const itemName = `Level ${level} Item ${i}`;
1227+
1228+
const item: ITreeItem = {
1229+
id: itemId,
1230+
name: itemName
1231+
};
1232+
1233+
// Add child items if not at the deepest level (level 6)
1234+
if (level < 6) {
1235+
item.childItems = generateLevel(level + 1, itemId);
1236+
}
1237+
1238+
totalItems++;
1239+
items.push(item);
1240+
itemKeys.add(itemId);
1241+
}
1242+
1243+
return items;
1244+
}
1245+
1246+
return generateLevel(1);
1247+
}
1248+
1249+
const treeData = generateTreeData();
1250+
console.log(`Total items: ${totalItems}`);
1251+
1252+
function HugeVirtualizedTreeRender<T extends object>(args: TreeProps<T>): JSX.Element {
1253+
let [expandedKeys, setExpandedKeys] = useState(new Set<Key>());
1254+
let expandAll = () => {
1255+
setExpandedKeys(itemKeys);
1256+
};
1257+
1258+
return (
1259+
<>
1260+
<button onClick={expandAll}>Expand All {totalItems}</button>
1261+
<Virtualizer layout={ListLayout} layoutOptions={{rowHeight: 30}}>
1262+
<TreeExampleDynamicRender {...args} expandedKeys={expandedKeys} onExpandedChange={setExpandedKeys} />
1263+
</Virtualizer>
1264+
</>
1265+
);
1266+
}
1267+
1268+
export const HugeVirtualizedTree: StoryObj<typeof VirtualizedTreeRender> = {
1269+
...TreeExampleDynamic,
1270+
args: {
1271+
...TreeExampleDynamic.args,
1272+
items: treeData
1273+
},
1274+
render: (args) => <HugeVirtualizedTreeRender {...args} />
1275+
};

0 commit comments

Comments
 (0)