Headless-Tree, and the Future of React-Complex-Tree

Lukas Bach
9 min read2 days ago

--

Hey everyone! I’m Lukas, I have created react-complex-tree and maintained it over the last couple of years. Quite some time ago, I started the development of a successor library, Headless Tree, which is now available in a Beta phase and will soon become available as stable release. In this blog post, I want to give some insights into this library, what it’s plans and future goals are, and how it relates to the future of react-complex-tree.

TLDR: Headless-Tree is a reimplementation of react-complex-tree, with the goal of providing the same features while improving on many of the disadvantages or compromises I had to take based on the architecture of react-complex-tree. At the moment, I have no plans of deprecating react-complex-tree. New features include virtualization support, better performance and smaller bundle size, easier interfaces, better customizability and more drag-related capabilities. Feel free to join the community on Discord to contribute with ideas, thoughts or PRs, or follow BlueSky for updates!

In the following, I will use the abbreviation RCT for react-complex-tree, and HT for Headless Tree.

Some history on react-complex-tree

I started working on RCT more than four years ago, with the goal of building a library for accessible tree views for the web. The motivation was a lack of libraries which are able to deliver on all important aspects of the expectations that users have from tree views, and since most of us are software developers who use powerful IDEs everyday, our expectations go pretty far. At the same time, the ecosystem didn’t really provide anything that is fully accessible, provides powerful drag-and-drop capabilities, supports hotkeys and multi select and so on. So my goal with RCT was to fill that gap. At the same time, the library should be unopinionated and well suited for as many projects as possible.

As such, the library only focused on bridging the gap between your structured tree data, and rendering the data. It was still your task to manage the data model below, which makes sense since that will differ a lot between use cases and a library design that is too specific in that aspect makes the library unsuited for many users. Also, rendering the data is up you, which is also important since we don’t want to force specific UI libraries on you.

While RCT gives you quite a bit of flexibility, the interface still feels inflexible in some ways. RCT provides a big React component which renders the entirety of the tree, and allows you to customize individual parts of the tree with custom render methods.

<UncontrolledTreeEnvironment
renderItem={(title, arrow, children) => (
<li {...context.itemContainerWithChildrenProps}>
<button
{...context.itemContainerWithoutChildrenProps}
{...context.interactiveElementProps}
>
{arrow}
{title}
</button>
{children}
</li>
)}
>
// ...
</UncontrolledTreeEnvironment>

The reason for not letting you render everything yourself, is that W3’s accessibility requirements for trees with computed properties rather strictly dictate how a tree should be rendered, and the recursive tree structure makes it hard to provide an easy-to-use interface for that, which is why RCT still manages the complicated accessibility-compliant tree render-structure for you.

<ul>
<li><a>1. level</a></li>
<li>
<a>1. Level</a>
<ul>
<li><a>2. Level</a></li>
</ul>
</li>
</ul>

The fact that RCT manages this aspect of rendering also means that certain performance aspects are out of your hand, and RCT-internal rerendering mechanics degrade performance quite a bit for certain tree structures. Also, virtualization is hard to achieve as library consumer since you have barely any control of how the overall structure is rendered. It would have been possible to implement virtualization as core part of the library, but I also didn’t want that since virtualization can be janky in many cases, and forcing it onto every user is also something that I don’t think would have made a lot of sense.

Why a new library?

All those short-comings of the library — Poor performance in certain use cases, no compatibility with virtualization, no full control over render-logic — are all things I realized over time are important, and I also realized that the current interface of RCT is mostly incompatible with solving those issues. At the same time, maintaining React Complex Tree for four years, fixing many bugs and working together with many contributors and users has taught me many things about what is important in production use cases, and what a tree library needs to have to succeed as battle-tested product. This is why I have decided to build a new library, Headless Tree, which aims to solve those shortcomings with a new API that is easier to work with.

I have solved the underlying accessibility way a different way: By using declared properties instead of computed properties, it is possible to achieve a W3C compliant tree implementation, that can use a flattened rendered list of tree items, making customization by users significantly easier and providing a trivial interface to virtualize the rendering with. With declared properties, the tree instead injects information about the tree structure into aria attributes, that are interpreted by accessibility tools the same way how DOM structure is interpreted for computed properties otherwise. This allows the library to actually give the rendering responsibility entirely into your hands. At the same time, the flat structure also makes the concept easily compatible with virtualization, so easy that you can just plop the data you get from HT directly into a virtualization library of your choice and just render its output instead.

<button aria-setsize="2" aria-posinset="0" aria-level="0">1. Level</button>
<button aria-setsize="2" aria-posinset="1" aria-level="0">1. Level</button>
<button aria-setsize="1" aria-posinset="0" aria-level="1">2. Level</button>

Headless Tree will still take the responsibility of computing all necessary props for each element including all those ARIA attributes. HT will then provide the its interface through a single hook instead of an overly-complicated component:

const tree = useTree<string>({
rootItemId: "folder",
getItemName: (item) => item.getItemData().name,
isItemFolder: (item) => !item.getItemData().isFolder,
dataLoader: {
getItem: (itemId) => myData[itemId],
getChildren: (itemId) => myData[itemId].childrenIds,
},
features: [ syncDataLoaderFeature, selectionFeature, hotkeysCoreFeature ],
});
return (
<div ref={tree.registerElement}>
{tree.getItems().map((item) => (
<div
key={item.getId()}
style={{ marginLeft: `${item.getItemMeta().level * 20}px` }}
>
<button
{...item.getProps()}
ref={item.registerElement}
>
{item.getItemName()}
</button>
</div>
))}
</div>
);

This new interface and the fact that rendering is now completely separated from the library itself, also has another benefit, which you might have guessed from its new name compared to react-complex-tree: There is no direct relation to React anymore, and support for other UI frameworks is totally viable! At the moment, the majority of HL lives in a framework-agnostic package @headless-tree/core , and the React hook is a very thin 50 LOC binding in a separate @headless-tree/react package. I haven't started to port the react bindings to other frameworks, but plan to do so in the future.

Also, I want to mention another thing at this point: If you have used Tanstack Table in the past, then both the public interface and the internal implementation of the library might be similar to what you’ve seen in the past. HT’s implementation and interface is largely motivated by Tanstack Table, with its modularity in terms of features, and the way how its hook-based interface works. Big thanks to Tanstack providing a motivation for how interfaces for modern framework-agnostic UI libraries should look like!

What does that mean for react-complex-tree?

Over the past years, I have continuously tried to use the time I had available to maintain react-complex-tree as good as I could. There definitely were areas that needed more attention, but honestly, I reached a point where I was pretty happy with the state of react-complex-tree quite some time ago. This meant that I haven’t really implemented new features for a long time now, but instead focused on maintenance and fixing bugs. I think that the issues reported in the past reflect that well, since most are bug reports that I do my best to fix once they come up. There are some open discussions about features like virtualization, but I provided some details above why those features are so hard to implement with RCT.

The goal of Headless Tree is to build a new library that provides the basis for delivering those features that I was unable to do with react-complex-tree. However, I do not plan to deprecate react-complex-tree at this time, nor will I stop working on bug fixes for issue reports that come in. If you are currently using react-complex-tree and find that it fulfills your requirements, there is no reason to switch away. I’ll continue maintenance for it, and plan to do so even after HT reached a mature state. New bigger features are not planned for RCT, but they have not been for a longer time now, instead I will focus on HT to provide those features, where it is more viable to do so.

However, if you are looking for a tree-library and found that RCT lacks certain aspects for your use case, or if you already integrated RCT and still lack certain features, or even if you were happy with RCT in the past and are curious about the future, I suggest having a look at Headless Tree, see what it has to offer and maybe even join its community and see if you are interested in contributing or maybe just engage in discussions.

Other new features with Headless Tree

In addition to the simplified interface and support for virtualization, there are many other things that are newly supported by Headless Tree.

RCT already supported dragging items between several tree instances within a single tree environment. HT breaks apart the boundaries of environments, and allows individual tree instances to define interfaces for how they interact with arbitrary external drag events. This can be used to hook up several similar or not-so-similar trees, but also to support dragging non-tree items into a tree, or accepting drag events in custom components while dragging tree items.

Connecting foreign drag events with Headless Tree

There are more hotkeys to customize, and it’s easier to customize them. In addition to that, it’s very easy to implement completely new hotkeys that do custom logic within a tree:

const tree = useTree<string>({
hotkeys: {
focusNextItem: {
hotkey: "ArrowRight",
},
focusPreviousItem: {
hotkey: "ArrowLeft",
},
customExpandAll: {
hotkey: "Ctrl+q",
handler: (e, tree) => {
tree.expandAll();
},
},
},
features: [
syncDataLoaderFeature,
selectionFeature,
hotkeysCoreFeature,
expandAllFeature,
],
});

RCT wasn’t terribly large, but certainly not a tiny library with a 16.8kB gzipped bundlesize, due to the many features it offered and which it exposed via a single symbol. Headless Tree is not just smaller at 9.4kB, but exposes its features as seperate variables that can manually be included or not, depending on what you need. This allows you to stay below 5kB in many cases, if you just use the features you need.

The feature composability also allows you write your own plugins to HT as new features, that add new capability to the tree functionality. It also makes it easier to customize Headless Tree, since you can write small features that just replace parts of what core features contribute, or copy the entire implementation of a feature such as drag-and-drop and change it exactly to how you want it to work.

When will Headless Tree become available as stable release?

Headless Tree is already in a pretty stable state. So far, I’ve handled HT as “alpha” state, meaning that I considered bugs to be expected and allowed patch version bumps to have breaking changes. As of now with the availability as Beta, I will start proper versioning and make efforts to keep the current API stable in future releases, and maintain semantic versioning for breaking changes. I would be very happy for you to try it out and let me know of any bugs you may encounter, and report them via Github Issues so I can look into them. You can find documentations on how to get started on the Headless Tree Homepage. You can also join the Headless Tree Discord to share your thoughts or interest to contribute. If you want to support the development of Headless Tree, you can also do so via my GitHub Sponsors Page or by sharing the word about it and starring it on GitHub.

Once I got some feedback on the current state of HT and fixed the initial bug reports that will come in, I plan to release it as stable release in the next two months.

--

--

Lukas Bach
Lukas Bach

Written by Lukas Bach

Software Engineer at GoTo, interested in TypeScript and React development, accessibility and infrastructure. You can find my projects on lukasbach.com

No responses yet