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 supports asynchronous data loading for lazy-loading tree nodes with loading states, error handling, and integration with virtual scrolling for large datasets.
Configure asynchronous data loading with comprehensive state management and event handling.
/**
* Asynchronous loading configuration
*/
interface AsyncLoadingConfig<TreeDataType extends BasicDataNode = DataNode> {
/** Function to load data for a tree node */
loadData?: (treeNode: EventDataNode<TreeDataType>) => Promise<any>;
/** Keys of nodes that have completed loading */
loadedKeys?: Key[];
}
/**
* Load data function signature
*/
type LoadDataFunction<TreeDataType extends BasicDataNode = DataNode> = (
treeNode: EventDataNode<TreeDataType>
) => Promise<any>;
/**
* Load completion event handler
*/
interface LoadEventHandler<TreeDataType extends BasicDataNode = DataNode> {
onLoad?: (
loadedKeys: Key[],
info: {
event: 'load';
node: EventDataNode<TreeDataType>;
},
) => void;
}Tree nodes automatically manage loading states during async operations.
/**
* Node loading state properties
*/
interface NodeLoadingState {
/** Whether the node is currently loading */
loading?: boolean;
/** Whether the node has completed loading */
loaded?: boolean;
}
/**
* Enhanced event data node with loading state
*/
interface LoadingEventDataNode<TreeDataType> extends EventDataNode<TreeDataType> {
/** Current loading state */
loading: boolean;
/** Whether loading has completed */
loaded: boolean;
}Usage Examples:
import React, { useState } from "react";
import Tree from "rc-tree";
interface AsyncNodeData {
key: string;
title: string;
isLeaf?: boolean;
children?: AsyncNodeData[];
}
const BasicAsyncLoading = () => {
const [treeData, setTreeData] = useState<AsyncNodeData[]>([
{
key: '0-0',
title: 'Expandable Node 1',
},
{
key: '0-1',
title: 'Expandable Node 2',
},
{
key: '0-2',
title: 'Leaf Node',
isLeaf: true,
},
]);
const [loadedKeys, setLoadedKeys] = useState<string[]>([]);
const loadData = async (treeNode: any): Promise<void> => {
const { key } = treeNode;
console.log('Loading data for node:', key);
// Simulate API call delay
await new Promise(resolve => setTimeout(resolve, 1500));
// Simulate potential loading failure
if (Math.random() < 0.1) {
throw new Error(`Failed to load data for ${key}`);
}
// Generate child nodes
const children: AsyncNodeData[] = [
{
key: `${key}-0`,
title: `Child 1 of ${key}`,
isLeaf: true,
},
{
key: `${key}-1`,
title: `Child 2 of ${key}`,
},
{
key: `${key}-2`,
title: `Child 3 of ${key}`,
isLeaf: true,
},
];
// Update tree data
setTreeData(prevData => {
const updateNode = (nodes: AsyncNodeData[]): AsyncNodeData[] => {
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);
});
// Mark as loaded
setLoadedKeys(prev => [...prev, key]);
};
return (
<Tree
prefixCls="rc-tree"
treeData={treeData}
loadData={loadData}
loadedKeys={loadedKeys}
onLoad={(keys, info) => {
console.log('Load completed:', keys, info);
}}
/>
);
};import React, { useState } from "react";
import Tree from "rc-tree";
interface ErrorHandlingNodeData {
key: string;
title: string;
isLeaf?: boolean;
hasError?: boolean;
errorMessage?: string;
children?: ErrorHandlingNodeData[];
}
const AsyncLoadingWithErrorHandling = () => {
const [treeData, setTreeData] = useState<ErrorHandlingNodeData[]>([
{ key: '0-0', title: 'Reliable Node' },
{ key: '0-1', title: 'Unreliable Node (50% fail rate)' },
{ key: '0-2', title: 'Always Fails Node' },
]);
const [loadedKeys, setLoadedKeys] = useState<string[]>([]);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const loadData = async (treeNode: any): Promise<void> => {
const { key } = treeNode;
try {
// Simulate different failure rates
let failureRate = 0;
if (key.includes('0-1')) failureRate = 0.5;
if (key.includes('0-2')) failureRate = 1.0;
if (Math.random() < failureRate) {
throw new Error(`Network error loading ${key}`);
}
// Simulate loading delay
await new Promise(resolve => setTimeout(resolve, 1000));
// Generate children
const children: ErrorHandlingNodeData[] = [
{ key: `${key}-0`, title: `Child 1 of ${key}`, isLeaf: true },
{ key: `${key}-1`, title: `Child 2 of ${key}`, isLeaf: true },
];
// Update tree data with successful load
setTreeData(prevData => {
const updateNode = (nodes: ErrorHandlingNodeData[]): ErrorHandlingNodeData[] => {
return nodes.map(node => {
if (node.key === key) {
return {
...node,
children,
hasError: false,
errorMessage: undefined,
};
}
if (node.children) {
return { ...node, children: updateNode(node.children) };
}
return node;
});
};
return updateNode(prevData);
});
setLoadedKeys(prev => [...prev, key]);
} catch (error) {
console.error('Load failed:', error);
// Update tree data with error state
setTreeData(prevData => {
const updateNode = (nodes: ErrorHandlingNodeData[]): ErrorHandlingNodeData[] => {
return nodes.map(node => {
if (node.key === key) {
return {
...node,
hasError: true,
errorMessage: error instanceof Error ? error.message : 'Unknown error',
children: [],
};
}
if (node.children) {
return { ...node, children: updateNode(node.children) };
}
return node;
});
};
return updateNode(prevData);
});
// Still mark as "loaded" to prevent infinite retry
setLoadedKeys(prev => [...prev, key]);
}
};
const titleRender = (node: ErrorHandlingNodeData) => (
<span>
{node.title}
{node.hasError && (
<span style={{ color: 'red', marginLeft: 8 }}>
❌ ({node.errorMessage})
</span>
)}
</span>
);
return (
<Tree
prefixCls="rc-tree"
treeData={treeData}
loadData={loadData}
loadedKeys={loadedKeys}
expandedKeys={expandedKeys}
onExpand={setExpandedKeys}
titleRender={titleRender}
/>
);
};import React, { useState, useCallback, useRef } from "react";
import Tree from "rc-tree";
interface CachedNodeData {
key: string;
title: string;
type: 'folder' | 'file';
isLeaf?: boolean;
children?: CachedNodeData[];
}
const AsyncLoadingWithCaching = () => {
const [treeData, setTreeData] = useState<CachedNodeData[]>([
{ key: 'root', title: 'Root Directory', type: 'folder' },
]);
const [loadedKeys, setLoadedKeys] = useState<string[]>([]);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
// Cache to store loaded data
const cacheRef = useRef<Map<string, CachedNodeData[]>>(new Map());
const loadData = useCallback(async (treeNode: any): Promise<void> => {
const { key } = treeNode;
// Check cache first
const cachedData = cacheRef.current.get(key);
if (cachedData) {
console.log('Using cached data for:', key);
// Update tree with cached data
setTreeData(prevData => {
const updateNode = (nodes: CachedNodeData[]): CachedNodeData[] => {
return nodes.map(node => {
if (node.key === key) {
return { ...node, children: cachedData };
}
if (node.children) {
return { ...node, children: updateNode(node.children) };
}
return node;
});
};
return updateNode(prevData);
});
setLoadedKeys(prev => [...prev, key]);
return;
}
console.log('Loading fresh data for:', key);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1200));
// Generate directory contents
const children: CachedNodeData[] = [
{ key: `${key}/documents`, title: 'Documents', type: 'folder' },
{ key: `${key}/images`, title: 'Images', type: 'folder' },
{ key: `${key}/readme.txt`, title: 'readme.txt', type: 'file', isLeaf: true },
{ key: `${key}/config.json`, title: 'config.json', type: 'file', isLeaf: true },
];
// Cache the loaded data
cacheRef.current.set(key, children);
// Update tree data
setTreeData(prevData => {
const updateNode = (nodes: CachedNodeData[]): CachedNodeData[] => {
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]);
}, []);
const clearCache = () => {
cacheRef.current.clear();
setLoadedKeys([]);
setExpandedKeys([]);
setTreeData([{ key: 'root', title: 'Root Directory', type: 'folder' }]);
};
const titleRender = (node: CachedNodeData) => (
<span>
{node.type === 'folder' ? '📁' : '📄'} {node.title}
{cacheRef.current.has(node.key) && (
<span style={{ color: 'green', marginLeft: 8 }}>📦</span>
)}
</span>
);
return (
<div>
<div style={{ marginBottom: 16 }}>
<button onClick={clearCache}>Clear Cache & Reset</button>
<p>Cache size: {cacheRef.current.size} items</p>
</div>
<Tree
prefixCls="rc-tree"
treeData={treeData}
loadData={loadData}
loadedKeys={loadedKeys}
expandedKeys={expandedKeys}
onExpand={setExpandedKeys}
titleRender={titleRender}
/>
</div>
);
};import React, { useState, useCallback, useMemo } from "react";
import Tree from "rc-tree";
interface SearchableNodeData {
key: string;
title: string;
description: string;
category: string;
isLeaf?: boolean;
children?: SearchableNodeData[];
}
const AsyncLoadingWithSearch = () => {
const [treeData, setTreeData] = useState<SearchableNodeData[]>([
{
key: 'products',
title: 'Products',
description: 'Product catalog',
category: 'root',
},
{
key: 'services',
title: 'Services',
description: 'Service offerings',
category: 'root',
},
]);
const [loadedKeys, setLoadedKeys] = useState<string[]>([]);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const loadData = useCallback(async (treeNode: any): Promise<void> => {
const { key } = treeNode;
// Simulate API search/load
await new Promise(resolve => setTimeout(resolve, 800));
let children: SearchableNodeData[] = [];
if (key === 'products') {
children = [
{ key: 'electronics', title: 'Electronics', description: 'Electronic devices and gadgets', category: 'product' },
{ key: 'books', title: 'Books', description: 'Physical and digital books', category: 'product' },
{ key: 'clothing', title: 'Clothing', description: 'Apparel and accessories', category: 'product' },
];
} else if (key === 'services') {
children = [
{ key: 'consulting', title: 'Consulting', description: 'Professional consulting services', category: 'service', isLeaf: true },
{ key: 'support', title: 'Support', description: 'Technical support services', category: 'service', isLeaf: true },
{ key: 'training', title: 'Training', description: 'Educational and training programs', category: 'service', isLeaf: true },
];
} else {
// Second level loading
children = Array.from({ length: 5 }, (_, index) => ({
key: `${key}-item-${index}`,
title: `${key} Item ${index + 1}`,
description: `Detailed description for ${key} item ${index + 1}`,
category: 'item',
isLeaf: true,
}));
}
// Filter children based on search term
if (searchTerm) {
children = children.filter(child =>
child.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
child.description.toLowerCase().includes(searchTerm.toLowerCase())
);
}
setTreeData(prevData => {
const updateNode = (nodes: SearchableNodeData[]): SearchableNodeData[] => {
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]);
}, [searchTerm]);
// Re-load data when search term changes
const handleSearchChange = (value: string) => {
setSearchTerm(value);
// Clear loaded keys to force re-loading with new search criteria
setLoadedKeys([]);
setExpandedKeys([]);
};
const titleRender = (node: SearchableNodeData) => {
const highlightText = (text: string, highlight: string) => {
if (!highlight) return text;
const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
return parts.map((part, index) =>
part.toLowerCase() === highlight.toLowerCase()
? <mark key={index}>{part}</mark>
: part
);
};
return (
<div>
<strong>{highlightText(node.title, searchTerm)}</strong>
<div style={{ fontSize: '0.8em', color: '#666' }}>
{highlightText(node.description, searchTerm)}
</div>
</div>
);
};
return (
<div>
<div style={{ marginBottom: 16 }}>
<input
type="text"
placeholder="Search items..."
value={searchTerm}
onChange={(e) => handleSearchChange(e.target.value)}
style={{ width: '100%', padding: 8 }}
/>
</div>
<Tree
prefixCls="rc-tree"
treeData={treeData}
loadData={loadData}
loadedKeys={loadedKeys}
expandedKeys={expandedKeys}
onExpand={setExpandedKeys}
titleRender={titleRender}
itemHeight={48} // Taller for two-line content
/>
</div>
);
};import React, { useState, useCallback } from "react";
import Tree from "rc-tree";
interface PaginatedNodeData {
key: string;
title: string;
hasMore?: boolean;
page?: number;
isLeaf?: boolean;
children?: PaginatedNodeData[];
}
const PaginatedAsyncLoading = () => {
const [treeData, setTreeData] = useState<PaginatedNodeData[]>([
{ key: 'dataset-1', title: 'Large Dataset 1 (1000+ items)', hasMore: true, page: 0 },
{ key: 'dataset-2', title: 'Large Dataset 2 (500+ items)', hasMore: true, page: 0 },
]);
const [loadedKeys, setLoadedKeys] = useState<string[]>([]);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const loadData = useCallback(async (treeNode: any): Promise<void> => {
const { key } = treeNode;
// Find the node to get current page
const findNode = (nodes: PaginatedNodeData[], targetKey: string): PaginatedNodeData | null => {
for (const node of nodes) {
if (node.key === targetKey) return node;
if (node.children) {
const found = findNode(node.children, targetKey);
if (found) return found;
}
}
return null;
};
const nodeData = findNode(treeData, key);
const currentPage = nodeData?.page || 0;
const pageSize = 20;
console.log(`Loading page ${currentPage + 1} for ${key}`);
// Simulate paginated API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Generate page of data
const startIndex = currentPage * pageSize;
const pageItems: PaginatedNodeData[] = Array.from({ length: pageSize }, (_, index) => ({
key: `${key}-item-${startIndex + index}`,
title: `Item ${startIndex + index + 1}`,
isLeaf: true,
}));
// Add "Load More" item if there are more pages
const totalItems = key === 'dataset-1' ? 1000 : 500;
const hasMore = (startIndex + pageSize) < totalItems;
if (hasMore) {
pageItems.push({
key: `${key}-load-more-${currentPage + 1}`,
title: `📄 Load More... (${totalItems - startIndex - pageSize} remaining)`,
hasMore: true,
page: currentPage + 1,
});
}
setTreeData(prevData => {
const updateNode = (nodes: PaginatedNodeData[]): PaginatedNodeData[] => {
return nodes.map(node => {
if (node.key === key) {
const existingChildren = node.children || [];
return {
...node,
children: [...existingChildren, ...pageItems],
hasMore: false, // This node itself no longer needs loading
};
}
if (node.children) {
return { ...node, children: updateNode(node.children) };
}
return node;
});
};
return updateNode(prevData);
});
setLoadedKeys(prev => [...prev, key]);
}, [treeData]);
return (
<Tree
prefixCls="rc-tree"
treeData={treeData}
loadData={loadData}
loadedKeys={loadedKeys}
expandedKeys={expandedKeys}
onExpand={setExpandedKeys}
/>
);
};import React, { useState, useCallback, useEffect, useRef } from "react";
import Tree from "rc-tree";
interface RealtimeNodeData {
key: string;
title: string;
lastUpdated?: Date;
isLive?: boolean;
children?: RealtimeNodeData[];
}
const RealtimeAsyncLoading = () => {
const [treeData, setTreeData] = useState<RealtimeNodeData[]>([
{ key: 'live-feeds', title: '📡 Live Data Feeds', isLive: true },
{ key: 'static-data', title: '📁 Static Data' },
]);
const [loadedKeys, setLoadedKeys] = useState<string[]>([]);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const intervalsRef = useRef<Map<string, NodeJS.Timeout>>(new Map());
const loadData = useCallback(async (treeNode: any): Promise<void> => {
const { key } = treeNode;
await new Promise(resolve => setTimeout(resolve, 500));
let children: RealtimeNodeData[] = [];
if (key === 'live-feeds') {
children = [
{ key: 'sensor-1', title: 'Temperature Sensor', isLive: true, lastUpdated: new Date() },
{ key: 'sensor-2', title: 'Humidity Sensor', isLive: true, lastUpdated: new Date() },
{ key: 'sensor-3', title: 'Pressure Sensor', isLive: true, lastUpdated: new Date() },
];
// Start real-time updates for live nodes
children.forEach(child => {
if (child.isLive && !intervalsRef.current.has(child.key)) {
const interval = setInterval(() => {
setTreeData(prevData => {
const updateNode = (nodes: RealtimeNodeData[]): RealtimeNodeData[] => {
return nodes.map(node => {
if (node.key === child.key) {
return {
...node,
lastUpdated: new Date(),
title: `${child.title.split(' (')[0]} (${Math.random().toFixed(2)})`,
};
}
if (node.children) {
return { ...node, children: updateNode(node.children) };
}
return node;
});
};
return updateNode(prevData);
});
}, 2000);
intervalsRef.current.set(child.key, interval);
}
});
} else {
children = [
{ key: 'static-1', title: 'Static File 1', lastUpdated: new Date('2024-01-01') },
{ key: 'static-2', title: 'Static File 2', lastUpdated: new Date('2024-01-02') },
];
}
setTreeData(prevData => {
const updateNode = (nodes: RealtimeNodeData[]): RealtimeNodeData[] => {
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]);
}, []);
// Cleanup intervals on unmount
useEffect(() => {
return () => {
intervalsRef.current.forEach(interval => clearInterval(interval));
intervalsRef.current.clear();
};
}, []);
const titleRender = (node: RealtimeNodeData) => (
<span>
{node.title}
{node.isLive && <span style={{ color: 'green' }}> 🔴</span>}
{node.lastUpdated && (
<span style={{ fontSize: '0.8em', color: '#666', marginLeft: 8 }}>
{node.lastUpdated.toLocaleTimeString()}
</span>
)}
</span>
);
return (
<div>
<h3>Real-time Data Tree</h3>
<p>Live sensors update every 2 seconds</p>
<Tree
prefixCls="rc-tree"
treeData={treeData}
loadData={loadData}
loadedKeys={loadedKeys}
expandedKeys={expandedKeys}
onExpand={setExpandedKeys}
titleRender={titleRender}
/>
</div>
);
};import React, { Component, ReactNode } from "react";
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
class AsyncTreeErrorBoundary extends Component<
{ children: ReactNode },
ErrorBoundaryState
> {
constructor(props: { children: ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: any) {
console.error('Tree async loading error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: 16, border: '1px solid #ff4d4f', borderRadius: 4 }}>
<h4>Tree Loading Error</h4>
<p>Something went wrong while loading tree data.</p>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
// Usage example
const SafeAsyncTree = () => (
<AsyncTreeErrorBoundary>
<AsyncLoadingTree />
</AsyncTreeErrorBoundary>
);Install with Tessl CLI
npx tessl i tessl/npm-rc-tree