react-vtree

repository·main·Indexed 19 days ago

https://github.com/lodin/react-vtree

A lightweight React library for efficiently rendering large tree structures using virtualization via react-window. It provides FixedSizeTree for uniform item sizes and VariableSizeTree for dynamic heights. The library utilizes a generator-based treeWalker algorithm for depth-first traversal and includes a recomputeTree method for programmatically updating node states. Requires React 18 or later.

Tokens
6.5K
Snippets
20
Records
27
Agent score
65%

What's inside react-vtree

  1. How the treeWalker algorithm works

    main

    The treeWalker is a generator function that builds the tree's internal representation. It follows a specific lifecycle:

    1. Initialization: The first yield must provide the root nodes of the tree.
    2. The Loop: A while(true) loop is used to process the rest of the tree.
    3. Parent-Child Interaction: Inside the loop, the generator calls yield. This pauses the generator and sends the current state to the tree component. In exchange, the tree component sends back a parent object.
    4. Yielding Children: The generator iterates through the parent.node.children and yields data for each child.
    5. Iteration: Once all children of a node are yielded, the loop continues, and the next yield will receive the next node to process (either a sibling, a child of a sibling, or an ancestor's sibling).

    Important: The treeWalker function is re-run whenever the treeWalker prop changes. To avoid performance issues, always memoize the function (e.g., with useCallback).

    function* treeWalker() {
      // 1. Yield root nodes
      yield getNodeData(rootNode, 0);
    
      while (true) {
        // 2. Receive a node to expand
        const parent = yield;
    
        // 3. Yield its children
        for (const child of parent.node.children) {
          yield getNodeData(child, parent.nestingLevel + 1);
        }
      }
    }
  2. Migrate from react-vtree 2.x.x to 3.x.x

    main

    Version 3 introduces a more optimized approach to tree building and openness state changes, particularly beneficial for large trees (e.g., ~1 million nodes). Migration requires updating the treeWalker logic, tree component props, the recomputeTree method, node interaction methods, and ID types.

    Key changes include:

    • treeWalker: Now exclusively for tree building. The Tree component manages openness state internally.
    • recomputeTree: Now accepts a list of nodes to change instead of an opennessState object, and introduces subtreeCallback.
    • Node Interaction: Replace toggle() with setOpen(boolean).
    • IDs: Node IDs must now be string types instead of Symbol to improve React rendering performance.
  3. Install react-vtree via npm or Yarn

    main

    To use react-vtree, you must install both react-vtree and its peer dependency react-window.

    Requirement: This package requires React 18 or later.

    npm

    npm i react-window react-vtree

    Yarn

    yarn add react-window react-vtree
  4. Implement a TreeWalker to traverse your data

    main

    The TreeWalker is a generator function that defines how the tree structure is traversed. It is the core mechanism used to discover nodes and their relationships. The walker is called by the tree's internal computer to build the flattened list of visible nodes.

    A TreeWalker must yield TreeWalkerValue objects (containing the node's data) or undefined to signal the end of a branch or the entire traversal.

    Key Concept: Depth-First Traversal The tree uses the generator to perform a depth-first search (DFS). When the generator yields a value, the tree creates a new record; when it yields undefined, the tree moves to the next sibling or parent.

    export type TreeWalker<TData extends NodeData, TMeta = {}> = () => Generator<
      TreeWalkerValue<TData, TMeta> | undefined,
      undefined,
      TreeWalkerValue<TData, TMeta>
    >;
  5. Use FixedSizeTree for trees with uniform item sizes

    main

    The FixedSizeTree component is used to render a tree where every node has the same height. It requires a treeWalker generator function to define the tree structure and a Node component to render each item.

    Key props:

    • treeWalker: A generator function that yields node data. It must be memoized (e.g., using useCallback) to prevent unnecessary re-computations.
    • itemSize: The fixed height of each node.
    • height & width: The dimensions of the tree container.
    • async: (boolean) If true, preserves tree state during re-builds, allowing for asynchronous data loading (e.g., loading branches on demand).
    • placeholder: A React node to show during the tree building process (useful for very large datasets to avoid UI freezes).
    • children: The component used to render each node. It receives data, isOpen, style, and setOpen.
    import { FixedSizeTree as Tree } from 'react-vtree';
    
    function* treeWalker() {
      // Step 1: Yield root nodes
      for (const node of treeNodes) {
        yield getNodeData(node, 0);
      }
    
      while (true) {
        // Step 2: Receive parent node
        const parent = yield;
    
        // Step 3: Yield children
        for (const child of parent.node.children) {
          yield getNodeData(child, parent.nestingLevel + 1);
        }
      }
    }
    
    const Node = ({ data: { isLeaf, name }, isOpen, style, setOpen }) => (
      <div style={style}>
        {!isLeaf && (
          <button type="button" onClick={() => void setOpen(!isOpen)}>
            {isOpen ? '-' : '+'}
          </button>
        )}
        <div>{name}</div>
      </div>
    );
    
    <Tree treeWalker={treeWalker} itemSize={30} height={150} width={300}>
      {Node}
    </Tree>
  6. Migrate recomputeTree method to version 3.x.x

    main

    The recomputeTree method signature has changed. Instead of an opennessState object, it now accepts a map where keys are node IDs and values are either a boolean or a configuration object.

    If you provide an object for a node ID, you can use the subtreeCallback to apply logic to every node in that node's subtree. This is useful for imitating old behaviors like useDefaultOpenness or useDefaultHeight.

    treeInstance.recomputeTree({
      'node-1': true,
      'node-2': {
        open: true,
        subtreeCallback(node, ownerNode) {
          if (node !== ownerNode) {
            node.isOpen = false;
          }
        },
      },
      'node-3': false,
    });
  7. Update treeWalker for version 3.x.x

    main

    In version 3, the treeWalker is used only for initial tree building. The Tree component handles the openness state internally. The generator should yield metadata for root nodes and then enter a loop to yield children for nodes as they are processed.

    To implement the new treeWalker, yield an object containing data (with id, isLeaf, isOpenByDefault, etc.), nestingLevel, and the node itself. When the generator receives a yielded value back, it should yield the children of that node.

    const getNodeData = (
      node: TreeNode,
      nestingLevel: number,
    ): TreeWalkerValue<MyNodeData, NodeMeta> => ({
      data: {
        id: node.id,
        isLeaf: node.children.length === 0,
        isOpenByDefault: true,
        name: node.name,
        nestingLevel,
      },
      nestingLevel,
      node,
    });
    
    function* treeWalker(): ReturnType<TreeWalker<MyNodeData, NodeMeta>> {
      for (const node of rootNodes) {
        yield getNodeData(node, 0);
      }
    
      while (true) {
        const parentMeta = yield;
    
        for (const child of parentMeta.node.children) {
          yield getNodeData(child, parentMeta.nestingLevel + 1);
        }
      }
    }
  8. Replace toggle() with setOpen(boolean) in node components

    main

    In version 3, the toggle function is removed from the node's props. It is replaced by setOpen(boolean), which provides more fine-grained control. To replicate the old toggle behavior, call setOpen(!isOpen).

    When using TypeScript, the node props can be typed using FixedSizeNodePublicState<MyNodeData>.

    import { type CSSProperties, type FC } from 'react';
    import { type FixedSizeNodePublicState } from 'react-vtree';
    
    type NodeProps = FixedSizeNodePublicState<MyNodeData> & {
      style: CSSProperties;
    };
    
    const Node: FC<NodeProps> = ({
      data: { isLeaf, name },
      isOpen,
      style,
      setOpen,
    }) => (
      <div style={style}>
        {!isLeaf && (
          <div>
            {/* Imitating the old `toggle` function behavior */}
            <button onClick={() => void setOpen(!isOpen)}>
              {isOpen ? '-' : '+'}
            </button>
          </div>
        )}
        <div>{name}</div>
      </div>
    );
  9. Update tree state with recomputeTree

    main

    The recomputeTree method allows you to programmatically update the openness state or other properties of nodes without rebuilding the entire tree from scratch. This is useful for bulk operations like "close all nodes under X".

    Usage: Pass an object where keys are node ids and values are rule objects.

    Rule Object Shape:

    • open: boolean: Sets the openness state for the owner node only.
    • subtreeCallback(node: FixedSizeNodePublicState, ownerNode: FixedSizeNodePublicState): void: A callback that runs against every node in the subtree (including the owner). Use this to apply logic to descendants.

    Note on Order: If multiple rules affect the same node, the order matters. Rules for children should be placed after rules for parents if you want the child rules to override the parent's subtreeCallback.

    const treeRef = useRef(null);
    
    // Close all nodes under 'root-1', then re-open 'child-4'
    treeRef.current?.recomputeTree({
      'root-1': {
        open: false,
        subtreeCallback(node, ownerNode) {
          if (node !== ownerNode) {
            node.isOpen = false;
          }
        },
      },
      'child-4': { open: true },
    });
    
    <Tree ref={treeRef} ... />
  10. Use VariableSizeTree for trees with dynamic item sizes

    main

    The VariableSizeTree component is used when nodes have different heights.

    Key differences from FixedSizeTree:

    • treeWalker: The yielded data must include a defaultHeight field within the data object.
    • itemSize: (Optional) A function (index: number) => number. If not provided, the tree uses the defaultHeight provided in the node's data.
    • Node props: The Node component receives an additional height property and a resize(newHeight: number, shouldForceUpdate?: boolean) function to dynamically update a node's height.
    • resetAfterId(id: string | symbol, shouldForceUpdate: boolean = false): A method to reset the tree layout after a node's size changes.
    import { VariableSizeTree as Tree } from 'react-vtree';
    
    const getNodeData = (node, nestingLevel) => ({
      data: {
        defaultHeight: 30, // mandatory for VariableSizeTree
        id: node.id,
        isOpenByDefault: true,
        // ... other data
      },
      nestingLevel,
      node,
    });
    
    // ... treeWalker implementation
    
    const Node = ({ data, isOpen, style, setOpen, height, resize }) => (
      <div style={style}>
        {/* Use height or resize as needed */}
        <div>{data.name}</div>
      </div>
    );
    
    <Tree treeWalker={treeWalker} height={150} width={300}>
      {Node}
    </Tree>
  11. Reference: FixedSizeTree Props

    main

    Properties available for the FixedSizeTree component. Many are inherited from react-window's FixedSizeList.

    async: boolean (allows tree to be asynchronous; preserves state between builds)
    buildingTaskTimeout: number (timeout for requestIdleCallback used with placeholder)
    children: component (the Node component)
    height: string | number
    itemSize: number
    layout: string = "vertical"
    listRef: Ref<FixedSizeList> (access to internal react-window list)
    placeholder: ReactNode | null (displayed during building process)
    rowComponent: component (custom Row component for FixedSizeList)
    treeWalker: *treeWalker() (iterator function)
    width: number | string
  12. Reference: FixedSizeNodeData and Public State

    main

    Data structures used by the treeWalker and the Node component.

    // FixedSizeNodeData: The shape of the 'data' field yielded by treeWalker
    interface FixedSizeNodeData {
      id: string | symbol;
      isOpenByDefault: boolean;
      // ... additional custom fields
    }
    
    // FixedSizeNodePublicState: The state available to the Node component
    interface FixedSizeNodePublicState<TData extends FixedSizeNodeData> {
      data: TData;
      isOpen: boolean;
      setOpen(state: boolean): Promise<void>;
    }