Tree

A hierarchical list structure component.

When To Use#

Almost anything can be represented in a tree structure. Examples include directories, organization hierarchies, biological classifications, countries, etc. The Tree component is a way of representing the hierarchical relationship between these things. You can also expand, collapse, and select a treeNode within a Tree.

Examples

parent 1
parent 1-0
leaf
leaf
parent 1-1
sss

The most basic usage, tell you how to use checkable, selectable, disabled, defaultExpandKeys, and etc.

expand codeexpand code
import { Tree } from 'infrad';
import type { DataNode, TreeProps } from 'infrad/lib/tree';
import React from 'react';

const treeData: DataNode[] = [
  {
    title: 'parent 1',
    key: '0-0',
    children: [
      {
        title: 'parent 1-0',
        key: '0-0-0',
        disabled: true,
        children: [
          {
            title: 'leaf',
            key: '0-0-0-0',
            disableCheckbox: true,
          },
          {
            title: 'leaf',
            key: '0-0-0-1',
          },
        ],
      },
      {
        title: 'parent 1-1',
        key: '0-0-1',
        children: [{ title: <span style={{ color: '#2673dd' }}>sss</span>, key: '0-0-1-0' }],
      },
    ],
  },
];

const App: React.FC = () => {
  const onSelect: TreeProps['onSelect'] = (selectedKeys, info) => {
    console.log('selected', selectedKeys, info);
  };

  const onCheck: TreeProps['onCheck'] = (checkedKeys, info) => {
    console.log('onCheck', checkedKeys, info);
  };

  return (
    <Tree
      checkable
      defaultExpandedKeys={['0-0-0', '0-0-1']}
      defaultSelectedKeys={['0-0-0', '0-0-1']}
      defaultCheckedKeys={['0-0-0', '0-0-1']}
      onSelect={onSelect}
      onCheck={onCheck}
      treeData={treeData}
    />
  );
};

export default App;
0-0
0-0-0
0-0-0-0
0-0-0-1
0-0-0-2
0-0-1
0-0-2
0-1
0-2

Drag treeNode to insert after the other treeNode or insert into the other parent TreeNode.

expand codeexpand code
import { Tree } from 'infrad';
import type { DataNode, TreeProps } from 'infrad/lib/tree';
import React, { useState } from 'react';

const x = 3;
const y = 2;
const z = 1;
const defaultData: DataNode[] = [];

const generateData = (_level: number, _preKey?: React.Key, _tns?: DataNode[]) => {
  const preKey = _preKey || '0';
  const tns = _tns || defaultData;

  const children = [];
  for (let i = 0; i < x; i++) {
    const key = `${preKey}-${i}`;
    tns.push({ title: key, key });
    if (i < y) {
      children.push(key);
    }
  }
  if (_level < 0) {
    return tns;
  }
  const level = _level - 1;
  children.forEach((key, index) => {
    tns[index].children = [];
    return generateData(level, key, tns[index].children);
  });
};
generateData(z);

const App: React.FC = () => {
  const [gData, setGData] = useState(defaultData);
  const [expandedKeys] = useState(['0-0', '0-0-0', '0-0-0-0']);

  const onDragEnter: TreeProps['onDragEnter'] = info => {
    console.log(info);
    // expandedKeys 需要受控时设置
    // setExpandedKeys(info.expandedKeys)
  };

  const onDrop: TreeProps['onDrop'] = info => {
    console.log(info);
    const dropKey = info.node.key;
    const dragKey = info.dragNode.key;
    const dropPos = info.node.pos.split('-');
    const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);

    const loop = (
      data: DataNode[],
      key: React.Key,
      callback: (node: DataNode, i: number, data: DataNode[]) => void,
    ) => {
      for (let i = 0; i < data.length; i++) {
        if (data[i].key === key) {
          return callback(data[i], i, data);
        }
        if (data[i].children) {
          loop(data[i].children!, key, callback);
        }
      }
    };
    const data = [...gData];

    // Find dragObject
    let dragObj: DataNode;
    loop(data, dragKey, (item, index, arr) => {
      arr.splice(index, 1);
      dragObj = item;
    });

    if (!info.dropToGap) {
      // Drop on the content
      loop(data, dropKey, item => {
        item.children = item.children || [];
        // where to insert 示例添加到头部,可以是随意位置
        item.children.unshift(dragObj);
      });
    } else if (
      ((info.node as any).props.children || []).length > 0 && // Has children
      (info.node as any).props.expanded && // Is expanded
      dropPosition === 1 // On the bottom gap
    ) {
      loop(data, dropKey, item => {
        item.children = item.children || [];
        // where to insert 示例添加到头部,可以是随意位置
        item.children.unshift(dragObj);
        // in previous version, we use item.children.push(dragObj) to insert the
        // item to the tail of the children
      });
    } else {
      let ar: DataNode[] = [];
      let i: number;
      loop(data, dropKey, (_item, index, arr) => {
        ar = arr;
        i = index;
      });
      if (dropPosition === -1) {
        ar.splice(i!, 0, dragObj!);
      } else {
        ar.splice(i! + 1, 0, dragObj!);
      }
    }
    setGData(data);
  };

  return (
    <Tree
      className="draggable-tree"
      defaultExpandedKeys={expandedKeys}
      draggable
      blockNode
      onDragEnter={onDragEnter}
      onDrop={onDrop}
      treeData={gData}
    />
  );
};

export default App;
parent 1
leaf
leaf

You can customize icons for different nodes.

expand codeexpand code
import {
  DownOutlined,
  FrownFilled,
  FrownOutlined,
  MehOutlined,
  SmileOutlined,
} from 'infra-design-icons';
import { Tree } from 'infrad';
import type { DataNode } from 'infrad/lib/tree';
import React from 'react';

const treeData: DataNode[] = [
  {
    title: 'parent 1',
    key: '0-0',
    icon: <SmileOutlined />,
    children: [
      {
        title: 'leaf',
        key: '0-0-0',
        icon: <MehOutlined />,
      },
      {
        title: 'leaf',
        key: '0-0-1',
        icon: ({ selected }) => (selected ? <FrownFilled /> : <FrownOutlined />),
      },
    ],
  },
];

const App: React.FC = () => (
  <Tree
    showIcon
    defaultExpandAll
    defaultSelectedKeys={['0-0-0']}
    switcherIcon={<DownOutlined />}
    treeData={treeData}
  />
);

export default App;
parent 1
parent 1-0
leaf
leaf
leaf
parent 1-1
parent 1-2

customize collapse/expand icon of tree node

expand codeexpand code
import { DownOutlined } from 'infra-design-icons';
import { Tree } from 'infrad';
import type { DataNode, TreeProps } from 'infrad/lib/tree';
import React from 'react';

const treeData: DataNode[] = [
  {
    title: 'parent 1',
    key: '0-0',
    children: [
      {
        title: 'parent 1-0',
        key: '0-0-0',
        children: [
          {
            title: 'leaf',
            key: '0-0-0-0',
          },
          {
            title: 'leaf',
            key: '0-0-0-1',
          },
          {
            title: 'leaf',
            key: '0-0-0-2',
          },
        ],
      },
      {
        title: 'parent 1-1',
        key: '0-0-1',
        children: [
          {
            title: 'leaf',
            key: '0-0-1-0',
          },
        ],
      },
      {
        title: 'parent 1-2',
        key: '0-0-2',
        children: [
          {
            title: 'leaf',
            key: '0-0-2-0',
          },
          {
            title: 'leaf',
            key: '0-0-2-1',
          },
        ],
      },
    ],
  },
];

const App: React.FC = () => {
  const onSelect: TreeProps['onSelect'] = (selectedKeys, info) => {
    console.log('selected', selectedKeys, info);
  };

  return (
    <Tree
      showLine
      switcherIcon={<DownOutlined />}
      defaultExpandedKeys={['0-0-0']}
      onSelect={onSelect}
      treeData={treeData}
    />
  );
};

export default App;
0-0
0-0-0
0-0-0-0
0-0-0-1
0-0-0-2
0-0-1
0-0-1-0
0-0-1-1
0-0-1-2
0-0-2
0-1
0-2

Controlled mode lets parent nodes reflect the status of child nodes more intelligently.

expand codeexpand code
import { Tree } from 'infrad';
import type { DataNode } from 'infrad/lib/tree';
import React, { useState } from 'react';

const treeData: DataNode[] = [
  {
    title: '0-0',
    key: '0-0',
    children: [
      {
        title: '0-0-0',
        key: '0-0-0',
        children: [
          { title: '0-0-0-0', key: '0-0-0-0' },
          { title: '0-0-0-1', key: '0-0-0-1' },
          { title: '0-0-0-2', key: '0-0-0-2' },
        ],
      },
      {
        title: '0-0-1',
        key: '0-0-1',
        children: [
          { title: '0-0-1-0', key: '0-0-1-0' },
          { title: '0-0-1-1', key: '0-0-1-1' },
          { title: '0-0-1-2', key: '0-0-1-2' },
        ],
      },
      {
        title: '0-0-2',
        key: '0-0-2',
      },
    ],
  },
  {
    title: '0-1',
    key: '0-1',
    children: [
      { title: '0-1-0-0', key: '0-1-0-0' },
      { title: '0-1-0-1', key: '0-1-0-1' },
      { title: '0-1-0-2', key: '0-1-0-2' },
    ],
  },
  {
    title: '0-2',
    key: '0-2',
  },
];

const App: React.FC = () => {
  const [expandedKeys, setExpandedKeys] = useState<React.Key[]>(['0-0-0', '0-0-1']);
  const [checkedKeys, setCheckedKeys] = useState<React.Key[]>(['0-0-0']);
  const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
  const [autoExpandParent, setAutoExpandParent] = useState<boolean>(true);

  const onExpand = (expandedKeysValue: React.Key[]) => {
    console.log('onExpand', expandedKeysValue);
    // if not set autoExpandParent to false, if children expanded, parent can not collapse.
    // or, you can remove all expanded children keys.
    setExpandedKeys(expandedKeysValue);
    setAutoExpandParent(false);
  };

  const onCheck = (checkedKeysValue: React.Key[]) => {
    console.log('onCheck', checkedKeysValue);
    setCheckedKeys(checkedKeysValue);
  };

  const onSelect = (selectedKeysValue: React.Key[], info: any) => {
    console.log('onSelect', info);
    setSelectedKeys(selectedKeysValue);
  };

  return (
    <Tree
      checkable
      onExpand={onExpand}
      expandedKeys={expandedKeys}
      autoExpandParent={autoExpandParent}
      onCheck={onCheck}
      checkedKeys={checkedKeys}
      onSelect={onSelect}
      selectedKeys={selectedKeys}
      treeData={treeData}
    />
  );
};

export default App;
Expand to load
Expand to load
Tree Node

To load data asynchronously when click to expand a treeNode.

expand codeexpand code
import { Tree } from 'infrad';
import React, { useState } from 'react';

interface DataNode {
  title: string;
  key: string;
  isLeaf?: boolean;
  children?: DataNode[];
}

const initTreeData: DataNode[] = [
  { title: 'Expand to load', key: '0' },
  { title: 'Expand to load', key: '1' },
  { title: 'Tree Node', key: '2', isLeaf: true },
];

// It's just a simple demo. You can use tree map to optimize update perf.
const updateTreeData = (list: DataNode[], key: React.Key, children: DataNode[]): DataNode[] =>
  list.map(node => {
    if (node.key === key) {
      return {
        ...node,
        children,
      };
    }
    if (node.children) {
      return {
        ...node,
        children: updateTreeData(node.children, key, children),
      };
    }
    return node;
  });

const App: React.FC = () => {
  const [treeData, setTreeData] = useState(initTreeData);

  const onLoadData = ({ key, children }: any) =>
    new Promise<void>(resolve => {
      if (children) {
        resolve();
        return;
      }
      setTimeout(() => {
        setTreeData(origin =>
          updateTreeData(origin, key, [
            { title: 'Child Node', key: `${key}-0` },
            { title: 'Child Node', key: `${key}-1` },
          ]),
        );

        resolve();
      }, 1000);
    });

  return <Tree loadData={onLoadData} treeData={treeData} />;
};

export default App;
showLine:

showIcon:

showLeafIcon:
parent 1
parent 1-0
leaf
multiple line title
multiple line title
leaf
parent 1-1
parent 1-2
parent 2

Tree with connected line between nodes, turn on by showLine, customize the preseted icon by switcherIcon.

expand codeexpand code
import { CarryOutOutlined, FormOutlined } from 'infra-design-icons';
import { Switch, Tree } from 'infrad';
import type { DataNode } from 'infrad/lib/tree';
import React, { useState } from 'react';

const treeData: DataNode[] = [
  {
    title: 'parent 1',
    key: '0-0',
    icon: <CarryOutOutlined />,
    children: [
      {
        title: 'parent 1-0',
        key: '0-0-0',
        icon: <CarryOutOutlined />,
        children: [
          { title: 'leaf', key: '0-0-0-0', icon: <CarryOutOutlined /> },
          {
            title: (
              <>
                <div>multiple line title</div>
                <div>multiple line title</div>
              </>
            ),
            key: '0-0-0-1',
            icon: <CarryOutOutlined />,
          },
          { title: 'leaf', key: '0-0-0-2', icon: <CarryOutOutlined /> },
        ],
      },
      {
        title: 'parent 1-1',
        key: '0-0-1',
        icon: <CarryOutOutlined />,
        children: [{ title: 'leaf', key: '0-0-1-0', icon: <CarryOutOutlined /> }],
      },
      {
        title: 'parent 1-2',
        key: '0-0-2',
        icon: <CarryOutOutlined />,
        children: [
          { title: 'leaf', key: '0-0-2-0', icon: <CarryOutOutlined /> },
          {
            title: 'leaf',
            key: '0-0-2-1',
            icon: <CarryOutOutlined />,
            switcherIcon: <FormOutlined />,
          },
        ],
      },
    ],
  },
  {
    title: 'parent 2',
    key: '0-1',
    icon: <CarryOutOutlined />,
    children: [
      {
        title: 'parent 2-0',
        key: '0-1-0',
        icon: <CarryOutOutlined />,
        children: [
          { title: 'leaf', key: '0-1-0-0', icon: <CarryOutOutlined /> },
          { title: 'leaf', key: '0-1-0-1', icon: <CarryOutOutlined /> },
        ],
      },
    ],
  },
];

const App: React.FC = () => {
  const [showLine, setShowLine] = useState<boolean | { showLeafIcon: boolean }>(true);
  const [showIcon, setShowIcon] = useState<boolean>(false);
  const [showLeafIcon, setShowLeafIcon] = useState<boolean>(true);

  const onSelect = (selectedKeys: React.Key[], info: any) => {
    console.log('selected', selectedKeys, info);
  };

  const onSetLeafIcon = (checked: boolean) => {
    setShowLeafIcon(checked);
    setShowLine({ showLeafIcon: checked });
  };

  const onSetShowLine = (checked: boolean) => {
    setShowLine(checked ? { showLeafIcon } : false);
  };

  return (
    <div>
      <div style={{ marginBottom: 16 }}>
        showLine: <Switch checked={!!showLine} onChange={onSetShowLine} />
        <br />
        <br />
        showIcon: <Switch checked={showIcon} onChange={setShowIcon} />
        <br />
        <br />
        showLeafIcon: <Switch checked={showLeafIcon} onChange={onSetLeafIcon} />
      </div>
      <Tree
        showLine={showLine}
        showIcon={showIcon}
        defaultExpandedKeys={['0-0-0']}
        onSelect={onSelect}
        treeData={treeData}
      />
    </div>
  );
};

export default App;
parent 0
leaf 0-0
leaf 0-1
parent 1
leaf 1-0
leaf 1-1

Built-in directory tree. multiple support ctrl(Windows) / command(Mac) selection.

expand codeexpand code
import { Tree } from 'infrad';
import type { DataNode, DirectoryTreeProps } from 'infrad/lib/tree';
import React from 'react';

const { DirectoryTree } = Tree;

const treeData: DataNode[] = [
  {
    title: 'parent 0',
    key: '0-0',
    children: [
      { title: 'leaf 0-0', key: '0-0-0', isLeaf: true },
      { title: 'leaf 0-1', key: '0-0-1', isLeaf: true },
    ],
  },
  {
    title: 'parent 1',
    key: '0-1',
    children: [
      { title: 'leaf 1-0', key: '0-1-0', isLeaf: true },
      { title: 'leaf 1-1', key: '0-1-1', isLeaf: true },
    ],
  },
];

const App: React.FC = () => {
  const onSelect: DirectoryTreeProps['onSelect'] = (keys, info) => {
    console.log('Trigger Select', keys, info);
  };

  const onExpand: DirectoryTreeProps['onExpand'] = (keys, info) => {
    console.log('Trigger Expand', keys, info);
  };

  return (
    <DirectoryTree
      multiple
      defaultExpandAll
      onSelect={onSelect}
      onExpand={onExpand}
      treeData={treeData}
    />
  );
};

export default App;
0-0
0-0-0
0-0-0-0
0-0-0-0-0
0-0-0-0-1
0-0-0-0-2
0-0-0-0-3
0-0-0-0-4
0-0-0-0-5
0-0-0-0-6
0-0-0-0-7
0-0-0-0-8
0-0-0-0-9

Use virtual list through height prop.

expand codeexpand code
import { Tree } from 'infrad';
import type { DataNode } from 'infrad/lib/tree';
import React from 'react';

const dig = (path = '0', level = 3) => {
  const list = [];
  for (let i = 0; i < 10; i += 1) {
    const key = `${path}-${i}`;
    const treeNode: DataNode = {
      title: key,
      key,
    };

    if (level > 0) {
      treeNode.children = dig(key, level - 1);
    }

    list.push(treeNode);
  }
  return list;
};

const treeData = dig();

const App: React.FC = () => <Tree treeData={treeData} height={233} defaultExpandAll />;

export default App;

API#

Tree props#

PropertyDescriptionTypeDefaultVersion
allowDropWhether to allow dropping on the node({ dropNode, dropPosition }) => boolean-
autoExpandParentWhether to automatically expand a parent treeNodebooleanfalse
blockNodeWhether treeNode fill remaining horizontal spacebooleanfalse
checkableAdd a Checkbox before the treeNodesbooleanfalse
checkedKeys(Controlled) Specifies the keys of the checked treeNodes (PS: When this specifies the key of a treeNode which is also a parent treeNode, all the children treeNodes of will be checked; and vice versa, when it specifies the key of a treeNode which is a child treeNode, its parent treeNode will also be checked. When checkable and checkStrictly is true, its object has checked and halfChecked property. Regardless of whether the child or parent treeNode is checked, they won't impact each otherstring[] | {checked: string[], halfChecked: string[]}[]
checkStrictlyCheck treeNode precisely; parent treeNode and children treeNodes are not associatedbooleanfalse
defaultCheckedKeysSpecifies the keys of the default checked treeNodesstring[][]
defaultExpandAllWhether to expand all treeNodes by defaultbooleanfalse
defaultExpandedKeysSpecify the keys of the default expanded treeNodesstring[][]
defaultExpandParentIf auto expand parent treeNodes when initbooleantrue
defaultSelectedKeysSpecifies the keys of the default selected treeNodesstring[][]
disabledWhether disabled the treebooleanfalse
draggableSpecifies whether this Tree or the node is draggable. Use icon: false to disable drag handler iconboolean | ((node: DataNode) => boolean) | { icon?: React.ReactNode | false, nodeDraggable?: (node: DataNode) => boolean }falseconfig: 4.17.0
expandedKeys(Controlled) Specifies the keys of the expanded treeNodesstring[][]
fieldNamesCustomize node title, key, children field nameobject{ title: title, key: key, children: children }4.17.0
filterTreeNodeDefines a function to filter (highlight) treeNodes. When the function returns true, the corresponding treeNode will be highlightedfunction(node)-
heightConfig virtual scroll height. Will not support horizontal scroll when enable thisnumber-
iconCustomize treeNode iconReactNode | (props) => ReactNode-
loadDataLoad data asynchronouslyfunction(node)-
loadedKeys(Controlled) Set loaded tree nodes. Need work with loadDatastring[][]
multipleAllows selecting multiple treeNodesbooleanfalse
rootClassNameClassName on the root elementstring-4.20.0
rootStyleStyle on the root elementCSSProperties-4.20.0
selectableWhether can be selectedbooleantrue
selectedKeys(Controlled) Specifies the keys of the selected treeNodesstring[]-
showIconShows the icon before a TreeNode's title. There is no default style; you must set a custom style for it if set to truebooleanfalse
showLineShows a connecting lineboolean | {showLeafIcon: boolean}false
switcherIconCustomize collapse/expand icon of tree nodeReactNode | (({ expanded: boolean }) => React.ReactNode)-renderProps: 4.20.0
titleRenderCustomize tree node title render(nodeData) => ReactNode-4.5.0
treeDataThe treeNodes data Array, if set it then you need not to construct children TreeNode. (key should be unique across the whole array)array<{ key, title, children, [disabled, selectable] }>-
virtualDisable virtual scroll when set to falsebooleantrue4.1.0
onCheckCallback function for when the onCheck event occursfunction(checkedKeys, e:{checked: bool, checkedNodes, node, event, halfCheckedKeys})-
onDragEndCallback function for when the onDragEnd event occursfunction({event, node})-
onDragEnterCallback function for when the onDragEnter event occursfunction({event, node, expandedKeys})-
onDragLeaveCallback function for when the onDragLeave event occursfunction({event, node})-
onDragOverCallback function for when the onDragOver event occursfunction({event, node})-
onDragStartCallback function for when the onDragStart event occursfunction({event, node})-
onDropCallback function for when the onDrop event occursfunction({event, node, dragNode, dragNodesKeys})-
onExpandCallback function for when a treeNode is expanded or collapsedfunction(expandedKeys, {expanded: bool, node})-
onLoadCallback function for when a treeNode is loadedfunction(loadedKeys, {event, node})-
onRightClickCallback function for when the user right clicks a treeNodefunction({event, node})-
onSelectCallback function for when the user clicks a treeNodefunction(selectedKeys, e:{selected: bool, selectedNodes, node, event})-

TreeNode props#

PropertyDescriptionTypeDefault
checkableWhen Tree is checkable, set TreeNode display Checkbox or notboolean-
disableCheckboxDisables the checkbox of the treeNodebooleanfalse
disabledDisables the treeNodebooleanfalse
iconCustomize icon. When you pass component, whose render will receive full TreeNode props as component propsReactNode | (props) => ReactNode-
isLeafDetermines if this is a leaf node(effective when loadData is specified). false will force trade TreeNode as a parent nodeboolean-
keyUsed with (default)ExpandedKeys / (default)CheckedKeys / (default)SelectedKeys. P.S.: It must be unique in all of treeNodes of the treestring(internal calculated position of treeNode)
selectableSet whether the treeNode can be selectedbooleantrue
titleTitleReactNode---

DirectoryTree props#

PropertyDescriptionTypeDefault
expandActionDirectory open logic, optional: false | click | doubleClickstring | booleanclick

Note#

Before 3.4.0: The number of treeNodes can be very large, but when checkable=true, it will increase the compute time. So, we cache some calculations (e.g. this.treeNodesStates) to avoid double computing. But, this brings some restrictions. When you load treeNodes asynchronously, you should render tree like this:

{
  this.state.treeData.length ? (
    <Tree>
      {this.state.treeData.map(data => (
        <TreeNode />
      ))}
    </Tree>
  ) : (
    'loading tree'
  );
}

Tree Methods#

NameDescription
scrollTo({ key: string | number; align?: 'top' | 'bottom' | 'auto'; offset?: number })Scroll to key item in virtual scroll

FAQ#

How to hide file icon when use showLine?#

File icon realize by using switcherIcon. You can overwrite the style to hide it: https://codesandbox.io/s/883vo47xp8

Why defaultExpandedAll not working on ajax data?#

default prefix prop only works when inited. So defaultExpandedAll has already executed when ajax load data. You can control expandedKeys or render Tree when data loaded to realize expanded all.

Virtual scroll limitation#

Virtual scroll only render items in visible region. Thus not support auto width (like long title with horizontal scroll).

What does disabled node work logic in the tree?#

Tree change its data by conduction. Includes checked or auto expanded, it will conduction state to parent / children node until current node is disabled. So if a controlled node is disabled, it will only modify self state and not affect other nodes. For example, a parent node contains 3 child nodes and one of them is disabled. When check the parent node, it will only check rest 2 child nodes. As the same, when check these 2 child node, parent will be checked whatever checked state the disabled one is.

This conduction logic prevent that modify disabled parent checked state by check children node and user can not modify directly with click parent which makes the interactive conflict. If you want to modify this conduction logic, you can customize it with checkStrictly prop.