CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-rc-tree

Tree UI component for React with selection, checkboxes, drag-drop, and virtual scrolling features

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

async-loading.mddocs/

Async Loading

RC Tree supports asynchronous data loading for lazy-loading tree nodes with loading states, error handling, and integration with virtual scrolling for large datasets.

Capabilities

Async Loading Configuration

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;
}

Loading States

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:

Basic Async Loading

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);
      }}
    />
  );
};

Async Loading with Error Handling

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}
    />
  );
};

Async Loading with Caching

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>
  );
};

Async Loading with Search

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>
  );
};

Advanced Async Loading Patterns

Lazy Loading with Pagination

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}
    />
  );
};

Real-time Data Loading

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>
  );
};

Best Practices

Error Boundaries for Async Loading

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

docs

async-loading.md

data-management.md

drag-drop.md

index.md

selection-checking.md

tree-component.md

tree-node.md

virtual-scrolling.md

tile.json