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

virtual-scrolling.mddocs/

Virtual Scrolling

RC Tree provides performance optimization for large tree datasets using virtual scrolling with configurable item heights and scroll behaviors, powered by rc-virtual-list.

Capabilities

Virtual Scrolling Configuration

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:

Basic Virtual Scrolling

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

Virtual Scrolling with Dynamic Content

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

Virtual Scrolling with Checkboxes and Selection

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

Programmatic Scrolling Control

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 Considerations

Virtual Scrolling Best Practices

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

Memory Optimization Example

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

Known Limitations

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

Workarounds and Solutions

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

Integration with Other Features

Virtual Scrolling + Async Loading

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

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