Tree UI component for React with selection, checkboxes, drag-drop, and virtual scrolling features
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
RC Tree provides performance optimization for large tree datasets using virtual scrolling with configurable item heights and scroll behaviors, powered by rc-virtual-list.
Enable virtual scrolling to handle large trees efficiently by only rendering visible nodes.
/**
* Virtual scrolling configuration options
*/
interface VirtualScrollConfig {
/** Enable virtual scrolling */
virtual?: boolean;
/** Fixed height of the tree container */
height?: number;
/** Fixed height of individual tree items */
itemHeight?: number;
/** Width of the scroll container */
scrollWidth?: number;
/** Scroll offset per item */
itemScrollOffset?: number;
}
/**
* Scroll control interface from rc-virtual-list
*/
interface ScrollTo {
/** Scroll to specific index */
(index?: number): void;
/** Scroll to specific index with alignment */
(index: number, align: 'top' | 'bottom' | 'auto'): void;
}Usage Examples:
import React, { useState, useMemo } from "react";
import Tree from "rc-tree";
const BasicVirtualScrolling = () => {
// Generate large dataset
const treeData = useMemo(() => {
const generateData = (level: number, parentKey: string, count: number): any[] => {
return Array.from({ length: count }, (_, index) => {
const key = `${parentKey}-${index}`;
const title = `Node ${key}`;
if (level > 0) {
return {
key,
title,
children: generateData(level - 1, key, 5),
};
}
return { key, title, isLeaf: true };
});
};
return generateData(3, '0', 100); // 100 root nodes, each with nested children
}, []);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
return (
<div>
<h3>Virtual Scrolling Tree ({treeData.length} root nodes)</h3>
<Tree
prefixCls="rc-tree"
virtual
height={400} // Fixed height container
itemHeight={24} // Fixed height per item
treeData={treeData}
expandedKeys={expandedKeys}
onExpand={setExpandedKeys}
defaultExpandParent={false}
/>
</div>
);
};import React, { useState, useMemo } from "react";
import Tree from "rc-tree";
interface VirtualNodeData {
key: string;
title: string;
description?: string;
children?: VirtualNodeData[];
}
const DynamicVirtualScrolling = () => {
const [searchTerm, setSearchTerm] = useState('');
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
// Generate large dataset with searchable content
const allData = useMemo(() => {
const generateNode = (id: number, parentPath: string = ''): VirtualNodeData => {
const key = parentPath ? `${parentPath}-${id}` : `${id}`;
return {
key,
title: `Item ${key}`,
description: `Description for item ${key} with some searchable content`,
children: id < 1000 ? Array.from({ length: 3 }, (_, i) =>
generateNode(i, key)
) : undefined,
};
};
return Array.from({ length: 500 }, (_, i) => generateNode(i));
}, []);
// Filter data based on search term
const filteredData = useMemo(() => {
if (!searchTerm) return allData;
const filterNodes = (nodes: VirtualNodeData[]): VirtualNodeData[] => {
return nodes.reduce((acc, node) => {
const matchesSearch = node.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
node.description?.toLowerCase().includes(searchTerm.toLowerCase());
let filteredChildren: VirtualNodeData[] = [];
if (node.children) {
filteredChildren = filterNodes(node.children);
}
if (matchesSearch || filteredChildren.length > 0) {
acc.push({
...node,
children: filteredChildren.length > 0 ? filteredChildren : node.children,
});
}
return acc;
}, [] as VirtualNodeData[]);
};
return filterNodes(allData);
}, [allData, searchTerm]);
const titleRender = (node: VirtualNodeData) => (
<div>
<strong>{node.title}</strong>
{node.description && (
<div style={{ fontSize: '0.8em', color: '#666' }}>
{node.description}
</div>
)}
</div>
);
return (
<div>
<div style={{ marginBottom: 16 }}>
<input
type="text"
placeholder="Search nodes..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ width: '100%', padding: 8 }}
/>
<p>Showing {filteredData.length} items</p>
</div>
<Tree
prefixCls="rc-tree"
virtual
height={500}
itemHeight={48} // Taller items for two-line content
treeData={filteredData}
titleRender={titleRender}
expandedKeys={expandedKeys}
onExpand={setExpandedKeys}
defaultExpandParent={false}
/>
</div>
);
};import React, { useState, useMemo } from "react";
import Tree from "rc-tree";
const VirtualScrollingWithInteractions = () => {
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
// Generate structured data
const treeData = useMemo(() => {
const departments = ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance'];
const teams = ['Frontend', 'Backend', 'DevOps', 'QA', 'Design'];
const people = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry'];
return departments.map((dept, deptIndex) => ({
key: `dept-${deptIndex}`,
title: `${dept} Department`,
children: teams.map((team, teamIndex) => ({
key: `dept-${deptIndex}-team-${teamIndex}`,
title: `${team} Team`,
children: people.map((person, personIndex) => ({
key: `dept-${deptIndex}-team-${teamIndex}-person-${personIndex}`,
title: `${person} (${team})`,
isLeaf: true,
})),
})),
}));
}, []);
const totalNodes = useMemo(() => {
let count = 0;
const countNodes = (nodes: any[]): void => {
nodes.forEach(node => {
count++;
if (node.children) {
countNodes(node.children);
}
});
};
countNodes(treeData);
return count;
}, [treeData]);
return (
<div>
<div style={{ marginBottom: 16 }}>
<h3>Organization Tree (Virtual Scrolling)</h3>
<p>Total nodes: {totalNodes}</p>
<p>Selected: {selectedKeys.length} | Checked: {checkedKeys.length}</p>
</div>
<Tree
prefixCls="rc-tree"
virtual
height={600}
itemHeight={28}
checkable
selectable
multiple
treeData={treeData}
selectedKeys={selectedKeys}
checkedKeys={checkedKeys}
expandedKeys={expandedKeys}
onSelect={(keys, info) => {
console.log('Virtual tree selection:', keys.length, 'items');
setSelectedKeys(keys);
}}
onCheck={(checked, info) => {
console.log('Virtual tree check:', Array.isArray(checked) ? checked.length : checked.checked.length, 'items');
const keys = Array.isArray(checked) ? checked : checked.checked;
setCheckedKeys(keys);
}}
onExpand={(keys) => {
console.log('Virtual tree expand:', keys.length, 'expanded');
setExpandedKeys(keys);
}}
defaultExpandParent={false}
/>
</div>
);
};import React, { useState, useRef, useMemo } from "react";
import Tree from "rc-tree";
const ProgrammaticScrolling = () => {
const treeRef = useRef<any>(null);
const [searchIndex, setSearchIndex] = useState(0);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
// Generate data with searchable items
const treeData = useMemo(() => {
return Array.from({ length: 200 }, (_, index) => ({
key: `item-${index}`,
title: `Searchable Item ${index}`,
isLeaf: true,
}));
}, []);
const scrollToItem = (index: number) => {
// Note: This is a conceptual example - actual scrollTo API may vary
if (treeRef.current && treeRef.current.scrollTo) {
treeRef.current.scrollTo(index);
}
};
const jumpToRandom = () => {
const randomIndex = Math.floor(Math.random() * treeData.length);
setSearchIndex(randomIndex);
scrollToItem(randomIndex);
};
const jumpToTop = () => {
scrollToItem(0);
setSearchIndex(0);
};
const jumpToBottom = () => {
const lastIndex = treeData.length - 1;
scrollToItem(lastIndex);
setSearchIndex(lastIndex);
};
return (
<div>
<div style={{ marginBottom: 16 }}>
<h3>Programmatic Scrolling Control</h3>
<div style={{ marginBottom: 8 }}>
<button onClick={jumpToTop} style={{ marginRight: 8 }}>
Jump to Top
</button>
<button onClick={jumpToRandom} style={{ marginRight: 8 }}>
Jump to Random
</button>
<button onClick={jumpToBottom} style={{ marginRight: 8 }}>
Jump to Bottom
</button>
</div>
<div>
<label>
Jump to index:
<input
type="number"
min="0"
max={treeData.length - 1}
value={searchIndex}
onChange={(e) => {
const index = parseInt(e.target.value);
setSearchIndex(index);
scrollToItem(index);
}}
style={{ marginLeft: 8, width: 80 }}
/>
</label>
</div>
</div>
<Tree
ref={treeRef}
prefixCls="rc-tree"
virtual
height={400}
itemHeight={30}
treeData={treeData}
expandedKeys={expandedKeys}
onExpand={setExpandedKeys}
/>
</div>
);
};/**
* Performance optimization guidelines for virtual scrolling
*/
interface VirtualScrollingBestPractices {
/** Use consistent itemHeight for best performance */
itemHeight: number;
/** Set reasonable container height (avoid viewport height) */
height: number;
/** Minimize re-renders by memoizing data and callbacks */
memoization: boolean;
/** Use expandedKeys state management efficiently */
expandedKeysOptimization: boolean;
}import React, { useState, useMemo, useCallback } from "react";
import Tree from "rc-tree";
const OptimizedVirtualTree = () => {
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
// Memoize large dataset generation
const treeData = useMemo(() => {
console.log('Generating tree data...');
return Array.from({ length: 10000 }, (_, index) => ({
key: `node-${index}`,
title: `Node ${index}`,
// Add some variety
disabled: index % 100 === 0,
checkable: index % 10 !== 0,
isLeaf: true,
}));
}, []);
// Memoize event handlers to prevent unnecessary re-renders
const handleExpand = useCallback((keys: string[]) => {
console.log('Expand changed:', keys.length);
setExpandedKeys(keys);
}, []);
const handleSelect = useCallback((keys: string[], info: any) => {
console.log('Selection changed:', keys.length);
setSelectedKeys(keys);
}, []);
// Memoize title renderer
const titleRender = useCallback((node: any) => {
return (
<span style={{
color: node.disabled ? '#ccc' : '#000',
fontWeight: selectedKeys.includes(node.key) ? 'bold' : 'normal',
}}>
{node.title}
{node.disabled && ' (disabled)'}
</span>
);
}, [selectedKeys]);
return (
<div>
<h3>Optimized Virtual Tree (10K items)</h3>
<p>Expanded: {expandedKeys.length} | Selected: {selectedKeys.length}</p>
<Tree
prefixCls="rc-tree"
virtual
height={500}
itemHeight={26}
selectable
multiple
treeData={treeData}
expandedKeys={expandedKeys}
selectedKeys={selectedKeys}
onExpand={handleExpand}
onSelect={handleSelect}
titleRender={titleRender}
/>
</div>
);
};/**
* Virtual scrolling limitations to be aware of
*/
interface VirtualScrollingLimitations {
/** Fixed item height required for best performance */
fixedItemHeight: boolean;
/** Dynamic height items may cause scroll jumping */
dynamicHeightIssues: boolean;
/** Drag and drop may have limitations with virtual items */
dragDropConstraints: boolean;
/** Complex animations may not work well */
animationLimitations: boolean;
}import React, { useState, useMemo } from "react";
import Tree from "rc-tree";
const VirtualScrollingWorkarounds = () => {
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
// Solution: Normalize item heights by controlling content
const treeData = useMemo(() => {
return Array.from({ length: 1000 }, (_, index) => ({
key: `item-${index}`,
title: `Item ${index}`, // Keep titles consistent length
// Avoid dynamic content that changes height
description: index % 2 === 0 ? 'Even item' : 'Odd item',
isLeaf: true,
}));
}, []);
// Solution: Custom title renderer that maintains consistent height
const titleRender = (node: any) => (
<div style={{
height: 24, // Fixed height
lineHeight: '24px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}>
<strong>{node.title}</strong>
<span style={{ color: '#666', marginLeft: 8 }}>
{node.description}
</span>
</div>
);
return (
<Tree
prefixCls="rc-tree"
virtual
height={400}
itemHeight={24} // Match the fixed height in titleRender
treeData={treeData}
titleRender={titleRender}
expandedKeys={expandedKeys}
onExpand={setExpandedKeys}
/>
);
};import React, { useState, useCallback } from "react";
import Tree from "rc-tree";
const VirtualAsyncTree = () => {
const [treeData, setTreeData] = useState([
{ key: 'root', title: 'Root (click to load)', children: [] },
]);
const [loadedKeys, setLoadedKeys] = useState<string[]>([]);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const loadData = useCallback(async (treeNode: any) => {
const { key } = treeNode;
// Simulate loading large dataset
await new Promise(resolve => setTimeout(resolve, 1000));
// Generate many children for virtual scrolling
const children = Array.from({ length: 500 }, (_, index) => ({
key: `${key}-child-${index}`,
title: `Child ${index}`,
isLeaf: true,
}));
setTreeData(prevData => {
const updateNode = (nodes: any[]): any[] => {
return nodes.map(node => {
if (node.key === key) {
return { ...node, children };
}
if (node.children) {
return { ...node, children: updateNode(node.children) };
}
return node;
});
};
return updateNode(prevData);
});
setLoadedKeys(prev => [...prev, key]);
}, []);
return (
<Tree
prefixCls="rc-tree"
virtual
height={400}
itemHeight={24}
treeData={treeData}
loadData={loadData}
loadedKeys={loadedKeys}
expandedKeys={expandedKeys}
onExpand={setExpandedKeys}
/>
);
};Install with Tessl CLI
npx tessl i tessl/npm-rc-tree