esc
navigate select
Browse documentation

Tree Data

TreeKit works with one shape: a plain array of TreeNode objects.

TreeNode
interface TreeNode<T = unknown> {
  id: string;            // unique across the ENTIRE tree
  label: string;         // rendered by the default node renderer
  children?: TreeNode<T>[];
  disabled?: boolean;
  hasChildren?: boolean; // for lazily-loaded nodes — see Async Loading
  icon?: ReactNode;      // per-node icon override
  data?: T;              // your own domain data, fully typed
}

Unique ids

Every id must be unique across the whole tree, not just among siblings — TreeKit indexes the tree by id internally for O(1) parent/ancestor lookups, and duplicate ids make selection and expansion state ambiguous. In development, TreeKit logs a console error if it finds duplicates; nothing crashes, but behavior around the duplicated node is undefined.

Attaching your own data

TreeNode is generic. Use the data field for anything domain-specific — a file path, a permission key, a database row — and it comes through fully typed wherever TreeKit hands you a node back (callbacks, renderNode, the utility functions):

interface FileMeta {
  path: string;
  sizeBytes: number;
}

const data: TreeNode<FileMeta>[] = [
  { id: "readme", label: "README.md", data: { path: "/README.md", sizeBytes: 1024 } },
];

<TreeKit
  data={data}
  onNodeDoubleClick={(node) => {
    // node.data is typed as FileMeta here
    openFile(node.data.path);
  }}
/>

TreeKit never mutates your data

data is treated as immutable. Expansion, checked, and selection state all live in separate id sets (either state you own, for controlled usage, or state TreeKit owns internally, for uncontrolled usage) — never written back onto your node objects. This means the same data array can safely be shared, memoized, or come straight from a server response.

Async-loaded children

The one exception is nodes loaded via onLoadChildren: TreeKit merges the returned children into an internal copy of the tree without touching your original objects. See Async Loading.

Leaf vs. branch nodes

A node is a "leaf" (no expand chevron, checkbox only) when it has no children array and no hasChildren flag. An empty array (children: []) is treated the same as a leaf — only a non-empty array or an explicit hasChildren: true renders a chevron.