Skip to main content
A note on stability

This project is currently in Alpha. Expect breaking changes in minor version bumps and incomplete changelogs. Bug reports and contributions are welcome!

Installation

To start using Headless Tree, install it to your project as dependency via

npm install @headless-tree/core @headless-tree/react

or

yarn add @headless-tree/core @headless-tree/react

During Alpha, releases are published on an irregular basis, and changelogs may not properly reflect all changes. The current main branch is always published to NPM under the snapshot tag, so you can use npm install @headless-tree/core@snapshot @headless-tree/react@snapshot to get the latest snapshot deployment.

Quick Start

The React Bindings of Headless Tree provide a hook useTree that creates a tree instance, which you can use to render your tree however you like. The instance provides methods to get the tree as a flat list of nodes, that you can easily render with custom logic. It provides accessibility props to make sure that, even if the tree is not rendered as hierarchical structure, screen readers still read it as such even if it is just rendered as flat list.

You can also use other frameworks than React. Use the createTree method from the core package to create a tree without a dependency on React. Documentation on that and bindings for other frameworks are planned for the future.

The useTree hook (or the createTree function from the core package, if you are not using the React bindings) return an instance of TreeInstance. The most important method on that instance is getItems(), which returns a flat list of all nodes in the tree. Those nodes are of type ItemInstance, which provides methods to interact with each item and get the necessary props to render it.

When rendering your tree, don't forget to

  • provide tree.registerElement as ref to the tree root, or otherwise call tree.registerElements with the root element of your tree during mounting and with null during unmount if you are not using React.
  • provide item.registerElement as ref to each item element, or otherwise call item.registerElements with the item element during mounting and with null during unmount if you are not using React.
  • Spread the return value of item.getProps() as props into your tree item elements.
  • You probably want to indent your items based on their depth, which you can get via item.getItemMeta().level.

Importing Features

The architecture of Headless Tree separates individual features into distinct objects, that need to be manually imported and added to your tree. They are exported from the @headless-tree/core package and need to be added to the features property in your tree config. This is done to design a clean architecture, allow customizability (you can easily write custom plugins that way, or overwrite parts of other plugins), and more importantly, allow you to only import the features you need. This is especially important for tree-shaking, as you can easily remove unused features from your bundle.

For example, if you want drag-and-drop support and hotkeys support, import the respective features and add them to your tree config:

import { useTree, dragAndDropFeature, hotkeysCoreFeature, syncDataLoaderFeature } from "@headless-tree/core";

useTree({
// ... other options
features: [
syncDataLoaderFeature,
dragAndDropFeature,
hotkeysCoreFeature,
]
})
Make sure to import all features you are using

Which features are imported and which not affects which methods are available on the tree instance and on the item instances, as well as which config options are respected by Headless Tree. However, the TypeScript Types always include all possible functions. If you are trying to use a feature which is not working or where methods are missing during runtime, check if you have included all necessary features in your tree.

All available features are documented with a dedicated guide on how to use them. In the sidebar, you can find each feature under its respective name in the "Features" category. The "Tree Core" and "Main Feature" features are special features that are always included and do not need to be imported.

Types

Headless Tree is written in TypeScript and provides full TypeScript support. The most important types to keep in mind are

  • TreeConfig: The configuration object that you pass to useTree or createTree.
  • TreeInstance: The instance of a tree that you get back from useTree or createTree.
  • ItemInstance: The instance of an item that you get back from the tree instance.
  • TreeState: The state of a tree instance, which you can access via tree.state (read more in the guide on managing State).

Note that some of the defined functions require that you include the respective feature in your tree config, see the section above.

Hooking up your data structure

As you saw in the previous example, we also added the syncDataLoaderFeature feature. Then, you can define how Headless Tree reads your tree with a dataLoader property in the tree config.

You need either that or the asyncDataLoaderFeature feature to hook up your data structure to Headless Tree. Note that, when using the asyncDataLoaderFeature, you need to provide a asyncDataLoader instead, which has a different interface (async methods instead of sync methods).

  • In the dataLoader.getItem property, you define a method that returns the payload of an item. The form of the payload is up to you, and can be provided as generic type to the useTree or createTree method.
  • In the dataLoader.getChildren property, you define a method that returns the IDs of the children of an item.

In addition to the dataLoader property, you also need to provide

  • the rootItemId property
  • a getItemName() method, which returns the display name of an item
  • a isItemFolder() method, which returns whether an item is a folder or not. An item being a folder means that it can be expanded.

For the latter two, the parameter provided to you is an item instance, so you can use all methods that you also have available during rendering. Use item.getItemData() to get the payload for the item that you provided.

const tree = useTree<ItemPayload>({
rootItemId: "root-item",
getItemName: (item) => item.getItemData().itemName,
isItemFolder: (item) => item.isFolder,
dataLoader: {
getItem: (itemId) => myDataStructure[itemId],
getChildren: (itemId) => myDataStructure[itemId].childrenIds,
},
features: [ syncDataLoaderFeature ],
});

In the prior demos, we mostly used string as simple payload type. The following demo shows how to use a more complex payload type, and how to use the getItemName and isItemFolder methods to get the display name and folder status of an item.

TODO-DOCS: Document the concept of data adapters, and mention their availability here.