Build a Tree from a Flat List of Nodes

intermediate
By FrontendAtlas Editorial · Updated Oct 15, 2026
Implement buildTree(items): turn a flat list of { id, parentId, ...fields } records into a forest of nodes where every node carries a children array. Siblings keep their input order, children may appear before their parents in the list, items whose parent is missing become roots, and the input records are never mutated. A public report of a ByteDance front end loop describes exactly this task ("given a flat array of objects, create a tree out of it"), and a public preview of an Apple frontend loop mentions nested tabs built from a flat list; this practice version fixes the contract so the tests can check order, orphans and immutability. Concepts: tree, data-structures, immutability, objects.

What you'll practice:

  • Linking records by id with a map instead of nested searches
  • Separating the copy pass from the link pass so order of appearance never matters
  • Stating the contract for orphans, leaves and extra fields before coding

Expected behavior

Your buildTree() implementation should:

  • Return an array of root nodes in input order, each with a children array
  • Attach every item to the node whose id equals its parentId, keeping siblings in input order
  • Attach children correctly even when they appear before their parent in the list
  • Treat an item with a null, undefined or unknown parentId as a root
  • Give leaves an empty children array and keep every other field of the item
  • Leave the input records unchanged; nodes are new objects

Arguments

  • items: Array<{ id: string | number; parentId?: string | number | null; [key: string]: unknown }> — Flat records in any order. parentId points at another item's id or is empty for roots.

Returns

TreeNode[] — The root nodes; every node is a copy of its record plus a children array.

Solution idea

Interview answer: do it in two passes. The first pass copies every record into a fresh node with an empty children array and stores it in a Map keyed by id, so lookups are constant time and the input is never touched. The second pass walks the records in input order and pushes each node into its parent's children, or into the roots array when the parent is missing. Because linking happens after every node exists, a child that appears before its parent still attaches, and because the second pass follows input order, siblings and roots keep their original order.

Examples
// Children can come before parents; siblings keep input order
buildTree([
  { id: 'a1', parentId: 'a', label: 'A1' },
  { id: 'root', parentId: null, label: 'Root' },
  { id: 'a', parentId: 'root', label: 'A' },
  { id: 'b', parentId: 'root', label: 'B' },
]);
// => [{ id: 'root', label: 'Root', parentId: null, children: [
//      { id: 'a', label: 'A', parentId: 'root', children: [{ id: 'a1', ..., children: [] }] },
//      { id: 'b', label: 'B', parentId: 'root', children: [] } ] }]

// An unknown parent makes the item a root
buildTree([{ id: 'x', parentId: 'missing' }]); // => [{ id: 'x', parentId: 'missing', children: [] }]

// Empty input
buildTree([]); // => []

Public interview reports

These public, self-reported interview accounts informed this practice version. The exercise is modelled on reported rounds, not copied from them, and the reports are not verified official questions or endorsements.