From 79027855a55e4bcfcbbea3ee1b4c8f08df4efa72 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 26 Aug 2025 10:47:59 +1000 Subject: [PATCH 1/2] fix: Speed up tree collection builder --- packages/react-aria-components/src/Tree.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-aria-components/src/Tree.tsx b/packages/react-aria-components/src/Tree.tsx index 6073eaa05d3..701ad3fc921 100644 --- a/packages/react-aria-components/src/Tree.tsx +++ b/packages/react-aria-components/src/Tree.tsx @@ -818,6 +818,7 @@ function flattenTree(collection: TreeCollection, opts: TreeGridCollectionO let flattenedRows: Node[] = []; // Need to count the items here because BaseCollection will return the full item count regardless if items are hidden via collapsed rows let itemCount = 0; + let parentLookup: Map = new Map(); let visitNode = (node: Node) => { if (node.type === 'item' || node.type === 'loader') { @@ -844,12 +845,13 @@ function flattenTree(collection: TreeCollection, opts: TreeGridCollectionO // Grab the modified node from the key map so our flattened list and modified key map point to the same nodes let modifiedNode = keyMap.get(node.key) || node; - if (modifiedNode.level === 0 || (modifiedNode.parentKey != null && expandedKeys.has(modifiedNode.parentKey) && flattenedRows.find(row => row.key === modifiedNode.parentKey))) { + if (modifiedNode.level === 0 || (modifiedNode.parentKey != null && expandedKeys.has(modifiedNode.parentKey) && parentLookup.get(modifiedNode.parentKey))) { if (modifiedNode.type === 'item') { itemCount++; } flattenedRows.push(modifiedNode); + parentLookup.set(modifiedNode.key, true); } } else if (node.type !== null) { keyMap.set(node.key, node as CollectionNode); From b0571eac345a55c47a33b2a0c86a7d73e00a20f8 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Tue, 26 Aug 2025 11:39:50 +1000 Subject: [PATCH 2/2] add story for verification --- .../stories/Tree.stories.tsx | 79 ++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/packages/react-aria-components/stories/Tree.stories.tsx b/packages/react-aria-components/stories/Tree.stories.tsx index b21afa9adeb..6a005514922 100644 --- a/packages/react-aria-components/stories/Tree.stories.tsx +++ b/packages/react-aria-components/stories/Tree.stories.tsx @@ -426,7 +426,7 @@ let defaultExpandedKeys = new Set(['projects', 'project-2', 'project-5', 'report const TreeExampleDynamicRender = (args: TreeProps): JSX.Element => { let treeData = useTreeData({ - initialItems: rows, + initialItems: args.items as any ?? rows, getKey: item => item.id, getChildren: item => item.childItems }); @@ -1196,3 +1196,80 @@ export const TreeWithDragAndDropVirtualized = { render: TreeDragAndDropVirtualizedRender, name: 'Tree with drag and drop (virtualized)' }; + + +interface ITreeItem { + id: string, + name: string, + childItems?: Iterable +} + +let totalItems = 0; +let itemKeys = new Set(); +/** + * Generates a tree data structure with 10 items per level and 6 levels deep. + * @returns Array of tree items with the specified structure. + */ +function generateTreeData(): Array { + /** + * Recursively generates tree items for a given level. + * @param level - Current depth level (1-6). + * @param parentId - Parent item ID for generating unique child IDs. + * @returns Array of tree items for this level. + */ + function generateLevel(level: number, parentId: string = ''): Array { + const items: ITreeItem[] = []; + const itemsPerLevel = 7; + + for (let i = 1; i < itemsPerLevel; i++) { + const itemId = parentId ? `${parentId}-${i}` : `level-${level}-item-${i}`; + const itemName = `Level ${level} Item ${i}`; + + const item: ITreeItem = { + id: itemId, + name: itemName + }; + + // Add child items if not at the deepest level (level 6) + if (level < 6) { + item.childItems = generateLevel(level + 1, itemId); + } + + totalItems++; + items.push(item); + itemKeys.add(itemId); + } + + return items; + } + + return generateLevel(1); +} + +const treeData = generateTreeData(); +console.log(`Total items: ${totalItems}`); + +function HugeVirtualizedTreeRender(args: TreeProps): JSX.Element { + let [expandedKeys, setExpandedKeys] = useState(new Set()); + let expandAll = () => { + setExpandedKeys(itemKeys); + }; + + return ( + <> + + + + + + ); +} + +export const HugeVirtualizedTree: StoryObj = { + ...TreeExampleDynamic, + args: { + ...TreeExampleDynamic.args, + items: treeData + }, + render: (args) => +};