Skip to content

Guide

Installation

sh
npm install he-tree-react
sh
pnpm add he-tree-react
sh
yarn add he-tree-react

Data Types

This library supports two types of data structures:

  • Flat data, which is a one-dimensional array. Similar to data stored in a database. Each item requires an id, a parent id, null means there is no parent. The order of flat data must be the same as the tree, you can use the sortFlatData method to sort the data when initializing the data.
    js
    [
      { id: 1, pid: null },
      { id: 2, pid: 1 },
      { id: 3, pid: 2 },
    ];
  • Tree data. Each node contain child nodes in an array. If id is not specified, this library will use the node's index in the tree as the id. When using tree data, you need to set dataType: 'tree'.
    js
    [
      {
        id: 1,
        children: [
          {
            id: 2,
            children: [{ id: 3 }],
          },
        ],
      },
    ];

The id, pid, children in the data are not fixed. In the options, use idKey, parentIdKey, childrenKey to indicate the corresponding key names in your data.

No Components

This library does not export components, but exports a hook useHeTree. Use the returned renderTree to render the tree. The advantage of this is that in addition to renderTree, useHeTree will also return some internal states and methods, which can be easily obtained.

js
import { useHeTree } from "he-tree-react";

export default function App() {
  const { renderTree } = useHeTree({...})
  return <div>
    {renderTree()}
  </div>
}

Options

useHeTree is the primary function used, its first parameter is an options object. The required options are data, and at least one of renderNode, renderNodeBox must be present. Other important options include:

  • dataType, indicating data type. Available values:
    • flat, default. Flat data.
    • tree, tree-shaped data.
  • idKey, parentIdKey, the default values are id and parent_id. Needed when using flat data. Although there are default values, it is better to explicitly state them.
  • childrenKey, the default is children. Needed when using tree-shaped data. Although there are default values, it is better to explicitly state them.
  • onChange, a function called when data changes, the parameter is new data. If your tree will not change then this is not required.
  • isFunctionReactive, boolean. Default false. useHeTree options include many callback functions, such as onChange, canDrop. isFunctionReactive can be used to control whether to listen for changes to these callback functions. If your callback functions and data change synchronously, you do not need to enable this. Otherwise, you need to enable this and use React's useCallback or useMemo to cache all your callback functions to avoid performance issues.

See the useHeTree API documentation for more information.

Tips

  • stat, information related to a single node. Most of the parameters in callback functions have stat. Refer to Stat API.
  • node, the data of the node. You can get node data through stat.node.
  • getStat, through this function you can get stat, the only parameter can be id, node, stat. This function is in the return object of useHeTree: const {getStat} = useHeTree({...}).
  • The code examples below have preview. These examples can be directly copied for use. Pay attention to the the highlighted lines in code.
  • The code examples below use the tsx format, if you need the js format, you can use any ts js online converter.

Use Flat Data

tsx
import { useHeTree, sortFlatData } from "he-tree-react";
import { useState } from 'react';

export default function BasePage() {
  const keys = { idKey: 'id', parentIdKey: 'parent_id' };
  const [data, setdata] = useState(() => sortFlatData([
    {
      id: 1,
      parent_id: null,
      name: "Root Category",
    },
    {
      id: 2,
      parent_id: 1,
      name: "Technology",
    },
    {
      id: 5,
      parent_id: 2,
      name: "Hardware",
    },
    {
      id: 10,
      parent_id: 5,
      name: "Computer Components",
    },
    {
      id: 4,
      parent_id: 2,
      name: "Programming",
    },
    {
      id: 8,
      parent_id: 4,
      name: "Python",
    },
    {
      id: 3,
      parent_id: 1,
      name: "Science",
    },
    {
      id: 7,
      parent_id: 3,
      name: "Biology",
    },
    {
      id: 6,
      parent_id: 3,
      name: "Physics",
    },
  ], keys));
  const { renderTree } = useHeTree({
    ...keys,
    data,
    dataType: 'flat',
    onChange: setdata,
    renderNode: ({ id, node, open, checked }) => <div>
      {node.name}
    </div>,
  })
  return <div>
    {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
  </div>
}
Preview

Use Tree-shaped Data

tsx
import { useHeTree } from "he-tree-react";
import { useState } from 'react';

export default function BasePage() {
  const [data, setdata] = useState(() => [
    {
      id: 1,
      name: "Root Category",
      children: [
        {
          id: 2,
          name: "Technology",
          children: [
            {
              id: 5,
              name: "Hardware",
              children: [
                {
                  id: 10,
                  name: "Computer Components",
                  children: [],
                },
              ],
            },
            {
              id: 4,
              name: "Programming",
              children: [
                {
                  id: 8,
                  name: "Python",
                  children: [],
                },
              ],
            },
          ],
        },
        {
          id: 3,
          name: "Science",
          children: [
            {
              id: 7,
              name: "Biology",
              children: [],
            },
            {
              id: 6,
              name: "Physics",
              children: [],
            },
          ],
        },
      ],
    },
  ]);
  const { renderTree } = useHeTree({
    data,
    dataType: 'tree',
    childrenKey: 'children',
    onChange: setdata,
    renderNode: ({ id, node, open, checked }) => <div>
      {node.name}
    </div>,
  })
  return <div>
    {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
  </div>
}
Preview

Custom Drag Trigger Element

You can add the draggable attribute to any child element of the node.

tsx
import { useHeTree, sortFlatData } from "he-tree-react";
import { useState } from 'react';

export default function BasePage() {
  const keys = { idKey: 'id', parentIdKey: 'parent_id' };
  // prettier-ignore
  const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
  const { renderTree } = useHeTree({
    ...keys,
    data,
    dataType: 'flat',
    onChange: setdata,
    renderNode: ({ id, node, open, checked, draggable }) => <div>
      <button draggable={draggable}>Drag</button>
      {node.name}
    </div>,
  })
  return <div>
    {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
  </div>
}
Preview

HTML and Style of Node

Node HTML:

html
<div
  draggable="true"
  data-key="1"
  data-level="1"
  data-node-box="true"
  style="padding-left: 0px;"
>
  <div>Node</div>
</div>

There are two div above. Use the renderNode option to control the rendering of the inner div. For example: renderNode: ({node}) => <div>{node.name}</div>.

The outer div is called nodeBox, don't modify its padding-left, padding-right. Use the indent option to control the indentation of the node. If you want to control the rendering of nodeBox or the drag placeholder, you can use the renderNodeBox option, which will override renderNode. The standard renderNodeBox is as follows:

tsx
renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
  <div {...attrs} key={attrs.key}>
    {isPlaceholder ? (
      <div
        className="he-tree-drag-placeholder"
        style={{ minHeight: "20px", border: "1px dashed blue" }}
      />
    ) : (
      <div>{/* node area */}</div>
    )}
  </div>
);

Lines 4 to 7 are drag-and-drop placeholder. Line 9 is node.

Custom Drag Placeholder and Node Box

tsx
import { useHeTree, sortFlatData } from "he-tree-react";
import { useState } from 'react';

export default function BasePage() {
  const keys = { idKey: 'id', parentIdKey: 'parent_id' };
  // prettier-ignore
  const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
  const { renderTree } = useHeTree({
    ...keys,
    data,
    dataType: 'flat',
    onChange: setdata,
    renderNodeBox: ({ stat, attrs, isPlaceholder }) => (
      <div {...attrs} key={attrs.key}>
        {isPlaceholder ? <div className="my-drag-placeholder">drop here</div>
          : <div className="mynode">{stat.node.name}</div>
        }
      </div>
    ),
  })
  return <div>
    {renderTree({ className: 'mytree', style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
    <style>{`
    .mytree [data-node-box]{
      padding: 5px 0;
    }
    .mytree [data-node-box]:hover{
      background-color: #eee;
    }
    .mytree .he-tree-drag-placeholder{
      height: 30px;
      line-height: 30px;
      text-align: center;
      border: 1px dashed red;
    }
    .mynode{
      padding-left:5px;
    }
    `}</style>
  </div>
}
Preview

Open & Close

  • Use the openIds option to indicate the open nodes.
  • The open status of the node can be obtained through stat.open.
  • The allIds returned by useHeTree contains the ids of all nodes.
  • This library exports methods that can expand all parents of one or multiple nodes. For flat data: openParentsInFlatData. For tree data: openParentsInTreeData.
tsx
import { useHeTree, sortFlatData, openParentsInFlatData } from "he-tree-react";
import type { Id } from "he-tree-react";
import { useState } from 'react';

export default function BasePage() {
  const keys = { idKey: 'id', parentIdKey: 'parent_id' };
  // prettier-ignore
  const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
  const [openIds, setopenIds] = useState<Id[] | undefined>([]);
  const handleOpen = (id: Id, open: boolean) => {
    if (open) {
      setopenIds([...(openIds || allIds), id]);
    } else {
      setopenIds((openIds || allIds).filter((i) => i !== id));
    }
  }
  const { renderTree, allIds } = useHeTree({
    ...keys,
    data,
    dataType: 'flat',
    onChange: setdata,
    openIds,
    renderNode: ({ id, node, open, checked, draggable }) => <div>
      <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
      {node.name} - {id}
    </div>,
  })
  return <div>
    <button onClick={() => setopenIds(allIds)}>Open All</button>
    <button onClick={() => setopenIds([])}>Close All</button>
    <button onClick={() => setopenIds(openParentsInFlatData(data, openIds || allIds, 8, keys))}>Open 'Python'</button>
    <button onClick={() => setopenIds(openParentsInFlatData(data, [], 8, keys))}>Only Open 'Python'</button>
    {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
  </div>
}
Preview

Checked

  • Use the option checkedIds to indicate the checked nodes.
  • The checked status of this node can be obtained through stat.checked.
  • This library exports methods that can get checkedIds for one or more nodes after the checked status changes. Flat data: updateCheckedInFlatData. Tree data: `updateCheckedInTreeData.
    • The update of this method to the node's checked is cascading. If you don't want to cascade updates, replace it with your own logic.
    • This method returns an array of length 2. The first item is all checked ids, and the second item is all semi-checked ids. If you don't need semi-checked, ignore the second item.
    • Semi-checked, that is, there are child nodes that are checked or semi-checked, and there are child nodes that are not checked.
tsx
import { useHeTree, sortFlatData, updateCheckedInFlatData } from "he-tree-react";
import type { Id } from "he-tree-react";
import { useState } from 'react';

export default function BasePage() {
  const keys = { idKey: 'id', parentIdKey: 'parent_id' };
  // prettier-ignore
  const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
  const [checkedIds, setcheckedIds] = useState<Id[]>([]);
  const [semiCheckedIds, setsemiCheckedIds] = useState<Id[]>([]);
  const handleChecked = (id: Id, checked: boolean) => {
    const r = updateCheckedInFlatData(data, checkedIds, id, checked, keys);
    setcheckedIds(r[0]);
    setsemiCheckedIds(r[1]);
  }
  const { renderTree } = useHeTree({
    ...keys,
    data,
    dataType: 'flat',
    onChange: setdata,
    checkedIds,
    renderNode: ({ id, node, open, checked, draggable }) => <div>
      <input type="checkbox" checked={checked || false} onChange={() => handleChecked(id, !checked)} />
      {node.name} - {id}
    </div>,
  })
  return <div>
    Checked: {JSON.stringify(checkedIds)} <br />
    Semi-Checked: {JSON.stringify(semiCheckedIds)}
    {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
  </div>
}
Preview

draggable & droppable

Use the following options to control:

tsx
import { useHeTree, sortFlatData } from "he-tree-react";
import { useState } from 'react';

export default function BasePage() {
  const keys = { idKey: 'id', parentIdKey: 'parent_id' };
  // prettier-ignore
  const [data, setdata] = useState(() => sortFlatData([{ id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
  const { renderTree } = useHeTree({
    ...keys,
    data,
    dataType: 'flat',
    onChange: setdata,
    renderNode: ({ id, node, open, checked, draggable }) => <div>
      {node.name} - {id}
    </div>,
    canDrag: ({ id }) => id === 2 ? true : (id === 3 ? false : undefined),
    canDrop: ({ id }) => id === 3 ? true : (id === 2 ? false : undefined),
    canDropToRoot: (index) => false,
  })
  return <div>
    {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
  </div>
}
Preview
  • The root node cannot be dropped.
  • Technology and its sub-nodes can be dragged. Science and its sub-nodes cannot be dragged.
  • Science and its sub-nodes can be dropped. Technology and its sub-nodes cannot be dropped.

Open when dragging over

Use the following options to control:

tsx
import { useHeTree, sortFlatData } from "he-tree-react";
import type { Id } from "he-tree-react";
import { useState } from 'react';

export default function BasePage() {
  const keys = { idKey: 'id', parentIdKey: 'parent_id' };
  // prettier-ignore
  const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
  const [openIds, setopenIds] = useState<Id[] | undefined>([1, 3]);
  const handleOpen = (id: Id, open: boolean) => {
    if (open) {
      setopenIds([...(openIds || allIds), id]);
    } else {
      setopenIds((openIds || allIds).filter((i) => i !== id));
    }
  }
  const { renderTree, allIds } = useHeTree({
    ...keys,
    data,
    dataType: 'flat',
    onChange: setdata,
    openIds,
    renderNode: ({ id, node, open, checked, draggable }) => <div>
      <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
      {node.name} - {id}
    </div>,
    dragOpen: true,
    onDragOpen(stat) {
      handleOpen(stat.id, true)
    },
  })
  return <div>
    {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
  </div>
}
Preview

Update Data

Due to the immutable nature of React, it is difficult to update flat data and tree data. For flat data, this library provides two methods to add nodes or delete nodes. If you want to perform more complex operations, or update tree data, it is recommended that you use immer.

sh
npm install immer use-immer
sh
pnpm add immer use-immer
sh
yarn add immer use-immer

Update Flat Data

addToFlatData. removeByIdInFlatData. These 2 methods will modify original data, so pass copy to it, or use immer.

tsx
import {
  useHeTree, sortFlatData,
  addToFlatData, removeByIdInFlatData
} from "he-tree-react";
import type { Id } from "he-tree-react";
import { useRef, useState } from 'react';

export default function BasePage() {
  const keys = { idKey: 'id', parentIdKey: 'parent_id' };
  // prettier-ignore
  const [data, setdata] = useState(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
  const add = (pid: Id) => {
    let id = parseInt(Math.random().toString().substring(2, 5));
    let newData = [...data];
    addToFlatData(newData, { id, parent_id: pid as number, name: "New" }, 0, keys)
    setdata(newData);
  }
  const remove = (id: Id) => {
    let newData = [...data];
    removeByIdInFlatData(newData, id as number, keys)
    setdata(newData);
  }
  const initialData = useRef<typeof data>();
  initialData.current = initialData.current || data;
  const { renderTree } = useHeTree({
    ...keys,
    data,
    dataType: 'flat',
    onChange: setdata,
    renderNode: ({ id, node, draggable }) => <div>
      <button draggable={draggable}>👉</button>
      {node.name} - {id} -
      <button onClick={() => add(id)}>+</button>
      <button onClick={() => remove(id)}>-</button>
    </div>,
  })
  return <div>
    <button onClick={() => setdata(initialData.current!)}>Restore</button>
    {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
  </div>
}
Preview

Update Flat Data with immer

Note, here we use useImmer instead of React's useState.

tsx
import {
  useHeTree, sortFlatData,
  addToFlatData, removeByIdInFlatData
} from "he-tree-react";
import type { Id } from "he-tree-react";
import { useRef } from 'react';
import { useImmer } from "use-immer";

export default function BasePage() {
  const keys = { idKey: 'id', parentIdKey: 'parent_id' };
  // prettier-ignore
  const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
  const add = (pid: Id) => {
    let id = parseInt(Math.random().toString().substring(2, 5));
    setdata(draft => {
      addToFlatData(draft, { id, parent_id: pid as number, name: "New" }, 0, keys)
    });
  }
  const remove = (id: Id) => {
    setdata(draft => {
      removeByIdInFlatData(draft, id as number, keys)
    })
  }
  const edit = (id: Id) => {
    let newName = prompt("Enter new name")
    setdata(draft => {
      if (newName) {
        draft.find(node => node.id === id)!.name = newName
      }
    })
  }
  const initialData = useRef<typeof data>();
  initialData.current = initialData.current || data;
  const { renderTree } = useHeTree({
    ...keys,
    data,
    dataType: 'flat',
    onChange: setdata,
    renderNode: ({ id, node, draggable }) => <div>
      <button draggable={draggable}>👉</button>
      {node.name} - {id} -
      <button onClick={() => add(id)}>+</button>
      <button onClick={() => remove(id)}>-</button>
      <button onClick={() => edit(id)}>Edit</button>
    </div>,
  })
  return <div>
    <button onClick={() => setdata(initialData.current!)}>Restore</button>
    {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
  </div>
}
Preview

Update Tree Data with immer

Note, here we use useImmer instead of React's useState. findTreeData is like Array.prototype.find.

tsx
import { useHeTree, findTreeData } from "he-tree-react";
import type { Id } from "he-tree-react";
import { useRef } from 'react';
import { useImmer } from "use-immer";

export default function BasePage() {
  const CHILDREN = 'children'
  const keys = { idKey: 'id', childrenKey: CHILDREN };
  // prettier-ignore
  const [data, setdata] = useImmer(() => [{ id: 1, name: "Root Category", children: [{ id: 2, name: "Technology", children: [{ id: 5, name: "Hardware", children: [{ id: 10, name: "Computer Components", children: [], },], }, { id: 4, name: "Programming", children: [{ id: 8, name: "Python", children: [], },], },], }, { id: 3, name: "Science", children: [{ id: 7, name: "Biology", children: [], }, { id: 6, name: "Physics", children: [], },], },], },]);
  const add = (pid: Id) => {
    let id = parseInt(Math.random().toString().substring(2, 5));
    setdata(draft => {
      findTreeData(draft, (node) => node.id === pid, CHILDREN)![CHILDREN].unshift({ id, name: "New", [CHILDREN]: [], })
    })
  }
  const remove = (id: Id, pid: Id | null) => {
    setdata(draft => {
      const children = findTreeData(draft, (node,) => node.id === pid, CHILDREN)![CHILDREN]
      children.splice(children.findIndex(t => t.id === id), 1)
    })
  }
  const edit = (id: Id) => {
    let newName = prompt("Enter new name")
    setdata(draft => {
      if (newName) {
        findTreeData(draft, (node) => node.id === id, CHILDREN)!.name = newName
      }
    })
  }
  const initialData = useRef<typeof data>();
  initialData.current = initialData.current || data;
  const { renderTree } = useHeTree({
    ...keys,
    data,
    dataType: 'tree',
    onChange: setdata,
    renderNode: ({ id, pid, node, draggable }) => <div>
      <button draggable={draggable}>👉</button>
      {node.name} - {id} -
      <button onClick={() => add(id)}>+</button>
      <button onClick={() => remove(id, pid)}>-</button>
      <button onClick={() => edit(id)}>Edit</button>
    </div>,
  })
  return <div>
    <button onClick={() => setdata(initialData.current!)}>Restore</button>
    {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
  </div>
}
Preview

Drag from External

Related options:

tsx
import { useHeTree, sortFlatData, addToFlatData } from "he-tree-react";
import { useImmer } from "use-immer";

export default function BasePage() {
  const keys = { idKey: 'id', parentIdKey: 'parent_id' };
  // prettier-ignore
  const [data, setdata] = useImmer(() => sortFlatData([{ id: 1, parent_id: null, name: "Root Category", }, { id: 2, parent_id: 1, name: "Technology", }, { id: 5, parent_id: 2, name: "Hardware", }, { id: 10, parent_id: 5, name: "Computer Components", }, { id: 4, parent_id: 2, name: "Programming", }, { id: 8, parent_id: 4, name: "Python", }, { id: 3, parent_id: 1, name: "Science", }, { id: 7, parent_id: 3, name: "Biology", }, { id: 6, parent_id: 3, name: "Physics", },], keys));
  const { renderTree, allIds } = useHeTree({
    ...keys,
    data,
    dataType: 'flat',
    onChange: setdata,
    renderNode: ({ id, node, open, checked, draggable }) => <div>
      {node.name} - {id}
    </div>,
    onExternalDragOver: (e) => true,
    onExternalDrop: (e, parentStat, index) => {
      setdata(draft => {
        const newNode = { id: 100 + data.length, parent_id: parentStat?.id ?? null, name: "New Node" }
        addToFlatData(draft, newNode, index, keys)
      })
    },
  })
  return <div>
    <button draggable={true}>Drag me in to the tree</button>
    {renderTree({ style: { width: '300px', border: '1px solid #555', padding: '20px' } })}
  </div>
}
Preview

Big Data

Use option virtual to enable virtual list. Remember to set height for tree.

tsx
import { useHeTree, sortFlatData } from "he-tree-react";
import type { Id } from "he-tree-react";
import { useState } from 'react';

export default function BasePage() {
  const keys = { idKey: 'id', parentIdKey: 'pid' };
  // prettier-ignore
  const [data, setdata] = useState(() => sortFlatData(createData(), keys));
  const [openIds, setopenIds] = useState<Id[] | undefined>([]);
  const handleOpen = (id: Id, open: boolean) => {
    if (open) {
      setopenIds([...(openIds || allIds), id]);
    } else {
      setopenIds((openIds || allIds).filter((i) => i !== id));
    }
  }
  const { renderTree, allIds } = useHeTree({
    ...keys,
    data,
    dataType: 'flat',
    onChange: setdata,
    openIds,
    virtual: true,
    renderNode: ({ id, node, open, checked, draggable }) => <div>
      <button onClick={() => handleOpen(id, !open)}>{open ? '-' : '+'}</button>
      {id}
    </div>,
  })
  return <div>
    {renderTree({ style: { width: '300px', height: '300px', border: '1px solid #555', padding: '20px' } })}
  </div>
}

// generate 10000 nodes
function createData() {
  const genId = () => result.length
  const result: { id: number, pid: number | null }[] = [];
  for (let i = 0; i < 1000; i++) {
    let id1 = genId()
    result.push({ id: id1, pid: null })
    for (let j = 0; j < 4; j++) {
      result.push({ id: genId(), pid: id1 })
    }
    let id2 = genId()
    result.push({ id: id2, pid: null })
    for (let j = 0; j < 4; j++) {
      result.push({ id: genId(), pid: id2 })
    }
  }
  return result;
}
Preview

Touch & Mobile Device

It is based on HTML5 Drag and Drop API. So it works in any device that supports Drag and Drop API. For others, you can try Drag and Drop API polyfill.

Notice

In mobile, user need touch and hold to trigger drag.

Others

  • Option direction: from right to left.
  • Option customDragImage: custom drag image.
  • Option rootId: the parent id of root nodes in flat data.
  • Option keepPlaceholder: whether to retain the drag placeholder node when dragging outside the tree. Default is false.
  • Function scrollToNode: Scroll to a node.

Last updated: