Build a Tree from a Flat List of Nodes
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[] — 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.
// 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.
- Blind: ByteDance new grad frontend engineer interview breakdown, January 2023
One ByteDance new grad front end account lists the first technical round's coding task as "given a flat array of objects, create a tree out of it". This drill is a practice version of that task; the contract for order, orphans and immutability is ours.
- LeetCode Discuss: Apple IC3 frontend interview experience, June 2025
Public search previews of this report list a nested-tabs rendering problem in React that required converting a flat list into a tree. The full report could not be read by our tools, so only that listing is used.
Solution
Overview
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.
Mental model: index first, link second. The alternative, filtering the list for each parent recursively, is shorter to write but quadratic and harder to make order-stable.
Approach 1: Two passes with a Map
Pass one builds nodes.set(item.id, { ...item, children: [] }) for every record. Pass two iterates the records again, looks up the node and its parent in the map, and pushes the node into parent.children or into roots when there is no parent, the parent is unknown, or the parent is the node itself. Time O(n), memory O(n) for the map and the copies.
export default function buildTree(items) {
const nodes = new Map();
for (const item of items) {
nodes.set(item.id, { ...item, children: [] });
}
const roots = [];
for (const item of items) {
const node = nodes.get(item.id);
const parent = item.parentId == null ? undefined : nodes.get(item.parentId);
if (parent && parent !== node) {
parent.children.push(node);
} else {
roots.push(node);
}
}
return roots;
}
type FlatItem = { id: string | number; parentId?: string | number | null } & Record<string, unknown>;
type TreeNode = FlatItem & { children: TreeNode[] };
export default function buildTree(items: FlatItem[]): TreeNode[] {
const nodes = new Map<string | number, TreeNode>();
for (const item of items) {
nodes.set(item.id, { ...item, children: [] });
}
const roots: TreeNode[] = [];
for (const item of items) {
const node = nodes.get(item.id) as TreeNode;
const parent = item.parentId == null ? undefined : nodes.get(item.parentId);
if (parent && parent !== node) {
parent.children.push(node);
} else {
roots.push(node);
}
}
return roots;
}
Approach 2: Recursive filter
Define attach(parentId) that filters the records whose parent is parentId and maps each to a node whose children are attach(item.id). Roots are the records whose parentId is empty, unknown, or self-referencing. Simple to read, but each level scans the whole list, so time is O(n²) in the worst case.
export default function buildTree(items) {
const ids = new Set(items.map((item) => item.id));
const isRoot = (item) => item.parentId == null || !ids.has(item.parentId) || item.parentId === item.id;
const attach = (parentId) =>
items
.filter((item) => (parentId === undefined ? isRoot(item) : !isRoot(item) && item.parentId === parentId))
.map((item) => ({ ...item, children: attach(item.id) }));
return attach(undefined);
}
type FlatItem = { id: string | number; parentId?: string | number | null } & Record<string, unknown>;
type TreeNode = FlatItem & { children: TreeNode[] };
export default function buildTree(items: FlatItem[]): TreeNode[] {
const ids = new Set(items.map((item) => item.id));
const isRoot = (item: FlatItem) => item.parentId == null || !ids.has(item.parentId) || item.parentId === item.id;
const attach = (parentId: string | number | undefined): TreeNode[] =>
items
.filter((item) => (parentId === undefined ? isRoot(item) : !isRoot(item) && item.parentId === parentId))
.map((item) => ({ ...item, children: attach(item.id) }));
return attach(undefined);
}
Notes & Pitfalls
- Linking during the first pass fails for children that appear before their parents.
- Mutating the input records by adding children breaks callers that reuse the list.
- Using an object as the index coerces numeric ids to strings; a Map keeps the id type.
- Dropping orphans silently hides data problems; treating them as roots keeps them visible.
- parentId undefined, null or pointing at an unknown id → root.
- A record whose parentId equals its own id → root, never its own child.
- Duplicate ids → the later record wins in the map; say so if asked.
- Empty list → empty array.
- Indexing records in a Map before linking them.
- Copying with spread to keep inputs immutable.
- Order-stable iteration over the original list.
Common mistakes on this challenge
Copy every node into a map first, then link by parentId
Linking only works once every node exists, and copying keeps the input untouched while giving every node a children array.
- First pass: `nodes.set(item.id, { ...item, children: [] })` for every record.
- Second pass over the original list: push each node into its parent's `children`, or into `roots` when `parentId` is empty or unknown.
- Never add `children` to the input objects; the nodes must be new objects.
Common interview follow-ups:
- How would you detect a cycle such as a → b → a and report it instead of looping?
- How would you render this tree as nested tabs or an accordion without re-walking it on every expand?
- How would you sort siblings by a field while keeping the algorithm linear?
- What changes if ids are numbers in the records but strings in parentId?
Resources
Similar questions
Guides
Preparing for interviews? Use Frontend Coding Challenges first, then move into a concrete Study Plan before targeted Company Prep.
Built in public
The code behind FrontendAtlas lives on GitHub. A star helps other frontend engineers find it.