he-tree-react: The React Drag and Drop Tree Component You Actually Want to Use

he-tree-react: The React Drag and Drop Tree Component You Actually Want to Use






he-tree-react: React Drag and Drop Tree Component Guide






he-tree-react: The React Drag and Drop Tree Component You Actually Want to Use

React
Tree View
Drag and Drop
Frontend
 ·  12 min read

Why Another Tree Library? Why he-tree-react?

If you’ve ever tried to build a React tree component from scratch, you know the pain. Recursive rendering feels elegant on slide two of a conference talk. In production, it becomes a haunted house of edge cases: collapsing parent nodes, keeping track of expanded state, and — the final boss — making nodes draggable and sortable without the entire tree losing its mind. Most open-source solutions either solve drag-and-drop poorly or force you into an opinionated data model that fights your backend schema at every turn.

he-tree-react takes a different approach. It separates the rendering concern from the data concern, giving you full control over how each node looks while handling all the tricky interaction logic internally. You’re not fighting the library to render a custom icon, add a context menu, or conditionally disable drag on certain node types. The library does what libraries should do: it solves the hard problem so you can focus on the interesting one.

Released and maintained by phphe, he-tree-react has quietly become one of the most capable React tree view libraries available on npm. It supports flat data arrays (no need to pre-nest your data), virtual scrolling for large datasets, multi-tree drag and drop, and fine-grained control over what can be dropped where. It is, in short, the tree library for people who have already been burned by tree libraries.

he-tree-react Installation and Initial Setup

Getting started with he-tree-react is refreshingly straightforward. There’s no peer-dependency maze, no Babel plugin required, and no secret handshake with Webpack. Open your terminal in a React project (version 16.8+ works fine; hooks are used internally) and run the following:

# npm
npm install he-tree-react

# yarn
yarn add he-tree-react

# pnpm
pnpm add he-tree-react

Once installed, import the core Tree component and the required stylesheet. The stylesheet is not optional — it handles the drag placeholder visuals and the baseline layout. Skipping it will leave you wondering why your tree looks like a JSON dump with hover states.

import { Tree } from 'he-tree-react';
import 'he-tree-react/style/default.css';

One of the more thoughtful decisions in the library’s design is its support for flat data arrays. Most tree components demand that you nest your data yourself — a tedious transformation you’ll inevitably do wrong the first time. With he-tree-react, you can feed it a flat array where each node has an id and a parentId, and the library handles the tree construction internally. This maps directly to how data typically comes out of a relational database or a REST API, saving you a normalization step.

const treeData = [
  { id: 1, text: 'Root Node',      parentId: null },
  { id: 2, text: 'Child One',      parentId: 1 },
  { id: 3, text: 'Child Two',      parentId: 1 },
  { id: 4, text: 'Grandchild One', parentId: 2 },
];

Your First Interactive Tree: A Minimal he-tree-react Example

Below is the minimal working example. It renders a tree from flat data, enables drag and drop, and logs the updated structure on every drop. This is the “Hello World” you want — not a toy with three hardcoded nodes, but something close enough to a real use case to be immediately useful.

import React, { useState } from 'react';
import { Tree } from 'he-tree-react';
import 'he-tree-react/style/default.css';

const initialData = [
  { id: 1, text: 'Documents',  parentId: null },
  { id: 2, text: 'Images',     parentId: null },
  { id: 3, text: 'Resume.pdf', parentId: 1 },
  { id: 4, text: 'Cover.docx', parentId: 1 },
  { id: 5, text: 'Photo.png',  parentId: 2 },
];

export default function App() {
  const [treeData, setTreeData] = useState(initialData);

  return (
    <Tree
      data={treeData}
      onChange={setTreeData}
      nodeKey="id"
      parentKey="parentId"
      renderNode={({ node }) => (
        <span style={{ padding: '4px 8px', cursor: 'grab' }}>
          {node.text}
        </span>
      )}
    />
  );
}

Let’s unpack what’s happening. The data prop accepts your flat array. onChange receives the updated flat array every time the user reorders or nests nodes via drag and drop — you simply pass it to your state setter and React re-renders the tree with the new structure. renderNode is your complete freedom: render anything in there. A styled-components div, an Ant Design row, a custom checkbox, a drag handle icon — it’s all yours.

Notice that there’s no manual event handling, no collision detection math, and no “lift state up” gymnastics beyond a single useState. This is by design. The he-tree-react drag and drop engine runs internally, computes the new tree structure, and delivers it to you fully resolved. Your only job is to decide what to do with that new data — persist it, validate it, or just update state.

Drag and Drop Deep Dive: How React Sortable Tree Actually Works Here

The drag and drop implementation in he-tree-react is powered by the HTML5 Drag and Drop API, not a canvas layer or a pointer-event polyfill. This is a pragmatic choice that keeps bundle size down and native feel intact. The library wraps the low-level events and translates them into meaningful tree operations: reorder siblings, change parent, move to root, move into a different subtree.

Where things get interesting is the onDrop callback and the allowDrop prop. allowDrop receives a context object describing the node being dragged, the potential new parent, and the proposed insertion index. Return true to allow the drop, false to reject it. This single hook handles an enormous range of real-world rules: prevent non-folder nodes from becoming parents, restrict nodes to a maximum depth, block certain node types from being mixed, or implement role-based editing where some nodes are locked for certain users.

<Tree
  data={treeData}
  onChange={setTreeData}
  nodeKey="id"
  parentKey="parentId"
  allowDrop={({ dragNode, targetParent, targetIndex }) => {
    // Prevent drop into leaf-type nodes
    if (targetParent && targetParent.type === 'file') return false;
    // Prevent nesting deeper than 3 levels
    if (targetParent && targetParent._depth >= 2) return false;
    return true;
  }}
  renderNode={({ node }) => <span>{node.text}</span>}
/>

Cross-tree drag and drop — dragging a node from one <Tree> instance into another — is enabled by assigning both trees the same dragContext value. This is the kind of feature that sounds simple to describe and is absolutely nightmarish to implement yourself. File manager interfaces, sidebar-to-canvas editors, role assignment panels: all of these common UI patterns become achievable without pulling in a second interaction library.

Working with React Hierarchical Data: Flat vs Nested

One of the quietest but most impactful features of he-tree-react is its native support for both flat and nested data formats. This matters more than it might seem at first. When your data comes from a SQL database, a GraphQL list query, or a REST endpoint, it almost always arrives flat — a list of objects with parent references. Converting that to a nested tree on the frontend before you can render it is a common source of bugs, and it makes synchronizing updates back to the server awkward.

With the nodeKey and parentKey props, he-tree-react handles that conversion internally. You store flat data in state, you receive flat data from onChange, and you send flat data back to your API. The hierarchical structure lives inside the component, not in your state management layer. This keeps your Redux store or Zustand atom lean and your data flow predictable.

If you do happen to have nested data — perhaps from a CMS that already returns a tree-shaped JSON — the library accepts that too. You can toggle between formats using the dataType prop. The important thing is consistency: pick one format for your data layer and let the library adapt, rather than writing your own adapters that you’ll have to maintain forever.

Advanced Usage: Virtualization, Custom Drag Handles, and Controlled State

For large datasets — think a file system explorer, an org chart with hundreds of employees, or a nested permission system — rendering every node in the DOM is a performance liability. he-tree-react ships with virtual scrolling support that renders only the visible nodes plus a small buffer. Enable it with the virtual prop and define a fixed row height via nodeMinHeight. The performance delta is dramatic: a tree with 10,000 nodes scrolls as smoothly as one with 50.

<Tree
  data={treeData}
  onChange={setTreeData}
  nodeKey="id"
  parentKey="parentId"
  virtual={true}
  nodeMinHeight={36}
  height={500}
  renderNode={({ node }) => <span>{node.text}</span>}
/>

Custom drag handles are another area where the library shines. By default, the entire node row is draggable. In complex UIs, this is often wrong — you want a dedicated handle so users can click on text, checkboxes, or inline edit fields without accidentally dragging the node across the tree. The renderNode callback receives a draggableProps object that you can spread onto any element inside your node to make only that element the drag initiator. This pattern is both clean and powerful.

Controlled state deserves special mention. Like any well-designed React component, he-tree-react supports both uncontrolled and fully controlled modes. In controlled mode, you manage the data, expandedKeys, and any other state externally. This means you can programmatically expand or collapse subtrees, bulk-select nodes, animate transitions, or sync tree state with a URL parameter — all without hacking the component’s internals. The library’s API surface respects React’s data flow rather than working around it.

he-tree-react vs. Other React Tree Libraries: An Honest Comparison

The React ecosystem isn’t short on tree components, and it’s worth being direct about the tradeoffs. react-sortable-tree was the dominant option for years, but it depends on react-dnd and react-virtualized — both large dependencies — and it hasn’t been actively maintained for some time. Its bundle footprint and occasional incompatibility with newer React versions have pushed many developers to look for alternatives.

@minoru/react-dnd-treeview is a newer, well-maintained option with TypeScript support, but it also wraps react-dnd and requires its provider setup. rc-tree from the Ant Design ecosystem is solid for read-heavy tree views but its drag-and-drop story is less first-class. react-arborist is excellent for performance-critical scenarios with a virtualized default, but its API is more opinionated about node structure.

he-tree-react sits in an interesting position: it has no heavy peer dependencies, supports both flat and nested data natively, ships its own drag engine without requiring a DnD context provider, and offers virtual scrolling as a built-in opt-in feature. The API surface is slightly larger than minimalist alternatives, but the documentation is thorough and the TypeScript types are accurate. For teams building complex, data-driven tree interfaces — file managers, project hierarchies, content editors — it consistently delivers with less friction than its competitors.

  • he-tree-react — flat data support, built-in DnD, virtual scroll, no heavy deps
  • react-arborist — excellent performance, virtualized by default, opinionated structure
  • @minoru/react-dnd-treeview — well-typed, requires react-dnd provider
  • react-sortable-tree — legacy, large deps, limited active maintenance
  • rc-tree — Ant Design ecosystem, strong for display, lighter DnD story

Practical he-tree-react Tutorial: Building a File Manager UI

Let’s put everything together in a realistic example: a simple file manager where folders can contain files, files cannot contain children, and drag and drop is constrained accordingly. This pattern appears constantly in content management systems, documentation platforms, and design tools, so it’s worth walking through completely.

import React, { useState } from 'react';
import { Tree } from 'he-tree-react';
import 'he-tree-react/style/default.css';

const FILES = [
  { id: 1, name: 'Projects',      type: 'folder', parentId: null },
  { id: 2, name: 'Archive',       type: 'folder', parentId: null },
  { id: 3, name: 'App.tsx',       type: 'file',   parentId: 1 },
  { id: 4, name: 'README.md',     type: 'file',   parentId: 1 },
  { id: 5, name: 'OldDocs.pdf',   type: 'file',   parentId: 2 },
  { id: 6, name: 'Components',    type: 'folder', parentId: 1 },
  { id: 7, name: 'Button.tsx',    type: 'file',   parentId: 6 },
];

const icon = (type) => type === 'folder' ? '📁' : '📄';

export default function FileManager() {
  const [data, setData] = useState(FILES);

  return (
    <div style={{ width: 320, border: '1px solid #e5e7eb', borderRadius: 8, padding: 16 }}>
      <h3 style={{ margin: '0 0 12px' }}>File Manager</h3>
      <Tree
        data={data}
        onChange={setData}
        nodeKey="id"
        parentKey="parentId"
        allowDrop={({ dragNode, targetParent }) => {
          // Files cannot become parents
          if (targetParent && targetParent.type === 'file') return false;
          return true;
        }}
        renderNode={({ node }) => (
          <span style={{
            display: 'flex',
            alignItems: 'center',
            gap: 6,
            padding: '4px 6px',
            borderRadius: 4,
            cursor: node.type === 'folder' ? 'pointer' : 'default',
          }}>
            {icon(node.type)} {node.name}
          </span>
        )}
      />
    </div>
  );
}

The key move here is in allowDrop: by checking targetParent.type === 'file', we prevent any node from being dropped inside a file node. Files stay as leaves; only folders can contain children. This single conditional implements the core business rule of a file system with zero additional state management.

From this base, you can extend in any direction. Add a right-click context menu by capturing onContextMenu on your render node. Add inline renaming by tracking an editingId in state and rendering an input instead of a span. Add optimistic API calls by hooking into onChange before calling setData. The component gets out of your way and lets you build the actual feature, which is the entire point of a good library.

TypeScript Support and Developer Experience

he-tree-react ships with TypeScript declarations included — no @types/ package needed. The types are accurate and useful: TreeData<T>, TreeNode<T>, and the various callback signatures are properly typed so your IDE’s autocomplete stays helpful throughout. If you’re working in a TypeScript-first codebase, you’ll appreciate that the generic type parameter flows through the entire component, meaning renderNode‘s node argument inherits your data type’s shape automatically.

interface FileNode {
  id: number;
  name: string;
  type: 'file' | 'folder';
  parentId: number | null;
}

// `node` inside renderNode is typed as FileNode — no casting needed
<Tree<FileNode>
  data={data}
  onChange={setData}
  nodeKey="id"
  parentKey="parentId"
  renderNode={({ node }) => <span>{node.name}</span>}
/>

The development experience is further helped by sensible default behaviors. Nodes are collapsible by default. The expand/collapse state is managed internally unless you opt into controlled mode. The drag placeholder provides a clear visual indicator of where the node will land, using a highlighted slot rather than a floating ghost that covers the surrounding nodes. These are the kinds of decisions that make a library feel considered rather than assembled.

Testing is worth a brief mention. Because he-tree-react outputs real DOM nodes and uses standard HTML5 drag events, it integrates well with React Testing Library. You can fire drag events with userEvent or fireEvent, query rendered nodes by their text content, and assert on the onChange callback’s arguments. Unlike canvas-based DnD solutions, there’s no need for special test environment configuration.

“The best tree library is the one you stop thinking about after day one — because it just does what you expect.”

Getting Started Checklist

Before you ship your tree-based UI, run through this checklist. It covers the most common integration oversights that result in bug reports on day one.

  • ✅ Import the default CSS — the drag indicator won’t render without it
  • ✅ Set nodeKey to a truly unique field — collisions cause silent render bugs
  • ✅ Define allowDrop if you have any business rules about valid drop targets
  • ✅ Use virtual={true} with height and nodeMinHeight for datasets over 200 nodes
  • ✅ If using TypeScript, pass your node type as a generic: <Tree<YourType>>
  • ✅ For cross-tree DnD, assign matching dragContext values to both instances
  • ✅ Test on touch devices if mobile support is a requirement — the HTML5 DnD API has limitations on iOS

Frequently Asked Questions

How do I install and set up he-tree-react in a React project?

Run npm install he-tree-react or yarn add he-tree-react in your project directory. Then import the Tree component and its stylesheet: import { Tree } from 'he-tree-react' and import 'he-tree-react/style/default.css'. Pass a flat data array with nodeKey and parentKey props, provide an onChange handler, and render your node content via renderNode. That’s the entire setup — no context providers, no Babel plugins, no separate DnD library required.

Does he-tree-react support drag and drop between different tree instances?

Yes. Cross-tree drag and drop is supported natively. Assign the same dragContext prop value to both <Tree> instances and they’ll recognize each other as valid drag targets. Handle the onChange callbacks on each tree to update their respective data arrays when a node moves between them. You can still use allowDrop per tree instance to restrict which nodes are accepted from the sibling tree.

How do I handle large hierarchical data sets with he-tree-react?

Enable virtual scrolling by setting virtual={true} on the <Tree> component, along with a fixed height (the viewport height in pixels) and nodeMinHeight (the height of each row). This instructs the library to render only visible rows, keeping DOM node count constant regardless of data size. For truly massive trees, combine virtual scrolling with lazy-loading: start with only root nodes in data and fetch children on the onExpand event, appending them to your flat array. This keeps initial load time and memory usage minimal.


Paolo Scoditti

Lascia un commento