esc
navigate select
Browse documentation

Async Loading

Mark a node as having children without providing them up front — TreeKit fetches on first expand.

Set hasChildren: true on a node instead of a children array, and pass onLoadChildren. TreeKit calls it the first time the node is expanded, shows a small spinner in the chevron slot while the promise is pending, and merges the result in without ever touching your original data:

Show code
const data = [{ id: "remote-root", label: "Remote Drive", hasChildren: true }];

async function fetchChildren(node) {
  const res = await fetch(`/api/files/${node.id}`);
  return res.json(); // TreeNode[]
}

<TreeKit data={data} onLoadChildren={fetchChildren} />

Expand "Remote Drive," then "Documents" or "Taxes" — each level loads independently, on demand.

PropTypeDefaultDescription
onLoadChildren(node: TreeNode) => Promise<TreeNode[]>Called once per node, the first time it's expanded while it has hasChildren and no children array yet.

Error handling & retry

If the promise rejects, TreeKit keeps the node expanded and shows "Failed to load" with a retry button in the default renderer, instead of silently failing or throwing. renderNode consumers get the same information via context.loadError and context.retryLoad():

renderNode={(ctx) => (
  <div>
    {ctx.node.label}
    {ctx.isLoading && <Spinner />}
    {ctx.loadError && (
      <button onClick={ctx.retryLoad}>Retry</button>
    )}
  </div>
)}

Loaded children merge structurally

Internally, TreeKit keeps a small map of loaded children and merges it against your data on read, reusing node object references for every branch untouched by a load. This means loading children for one node doesn't invalidate memoization anywhere else in the tree.

A node is only re-fetched if it's collapsed and re-expanded and the previous load hasn't already populated it — once loaded, a node's children persist for the lifetime of the component (or until you change data).