AniUI Academy

Flattening Nested Comments and Building a File Tree

Two common frontend tasks are secretly the same tree algorithm run in opposite directions — collapsing a nested reply thread into a flat list, and rebuilding a tree from flat data.

10 min read

Two tasks that show up constantly in real frontend work — rendering a nested comment thread, and building a file-explorer UI from an API's flat list of paths — are, underneath, the same tree operation run in opposite directions. This lesson works through both, and the payoff is recognizing that relationship rather than treating them as two things to memorize separately.

Flattening a nested comment tree

A comments API often returns genuinely nested data — each comment carries its replies, which carry their own replies:

const thread = {
  id: 1, text: "Great post!",
  replies: [
    { id: 2, text: "Agreed.", replies: [
      { id: 3, text: "Same here.", replies: [] },
    ]},
    { id: 4, text: "Disagree.", replies: [] },
  ],
};

Rendering this as a flat, indentation-aware list (a common UI pattern for deep threads) means flattening it — a direct application of DFS from two lessons ago, carrying the depth along as it recurses:

function flattenComments(comment, depth = 0, result = []) {
  result.push({ id: comment.id, text: comment.text, depth });
  for (const reply of comment.replies) {
    flattenComments(reply, depth + 1, result);
  }
  return result;
}
 
flattenComments(thread);
// [{id:1, depth:0}, {id:2, depth:1}, {id:3, depth:2}, {id:4, depth:1}]

Every comment is visited exactly once (a DFS preorder walk, node before children) and written exactly once into result. That's O(n) time and O(n) space for n total comments across every depth — you can't flatten n items into fewer than n output entries, so linear is not just achieved here, it's the theoretical floor for this problem.

Building a nested tree from a flat list

The reverse direction: an API returns a flat list of paths, and you need a nested tree to drive a file-explorer UI:

const paths = [
  "/src/index.js",
  "/src/components/Button.js",
  "/src/components/Modal.js",
  "/README.md",
];
function buildFileTree(paths) {
  const root = { name: "/", children: new Map() };
 
  for (const path of paths) {
    const segments = path.split("/").filter(Boolean); // ["src", "index.js"], etc.
    let current = root;
 
    for (const segment of segments) {
      if (!current.children.has(segment)) {
        current.children.set(segment, { name: segment, children: new Map() });
      }
      current = current.children.get(segment);
    }
  }
 
  return root;
}

For each path, walk down the tree one segment at a time, creating a folder node whenever one doesn't already exist at that position, keyed by name in a Map (the "check has(), create if missing" pattern from the grouping lesson, applied to tree nodes instead of array buckets). The cost of placing one path is proportional to that path's own depth — a deeply nested path costs more to place than a shallow one — and the Map-based lookup at each level keeps each step O(1) rather than searching an array of existing children.

Why the Map matters: a variant that's genuinely worse

A related, very common real-world version of this problem: a flat list where each item carries an explicit parentId instead of a full path (exactly how many CMS and comment APIs shape their data):

const flatComments = [
  { id: 1, parentId: null, text: "Great post!" },
  { id: 2, parentId: 1, text: "Agreed." },
  { id: 3, parentId: 2, text: "Same here." },
  { id: 4, parentId: 1, text: "Disagree." },
];
 
function buildTreeFromParentIds(flatItems) {
  const byId = new Map(flatItems.map((item) => [item.id, { ...item, replies: [] }]));
  const roots = [];
 
  for (const item of byId.values()) {
    if (item.parentId === null) {
      roots.push(item);
    } else {
      byId.get(item.parentId).replies.push(item); // O(1) parent lookup
    }
  }
 
  return roots;
}

The byId map is what keeps this O(n): finding an item's parent is an O(1) map lookup, done once per item, for O(n) total. Without it — searching the partially-built tree for the right parent node on every item instead — each placement could cost up to O(n) in the worst case, making the whole build O(n²). This is exactly the hashing part of this course's central lesson, applied one more time: replace a search with a lookup, and an accidental quadratic becomes a genuine linear.

Try it yourself
Loading playground...

What to remember

  • Flattening a nested tree into a flat, depth-annotated list is a direct DFS application — O(n) time and O(n) space, the theoretical minimum for producing n output items.
  • Building a nested tree from flat data is the inverse operation, and its cost per item is proportional to that item's depth (path-based) or is O(1) (parentId-based, with a map).
  • When flat data references its parent by id, keying a Map by id for O(1) parent lookups is what keeps the whole build O(n) — without it, repeated searches for the right parent make it O(n²).
  • Recognizing "flatten" and "rebuild" as inverse operations on the same tree/list relationship turns two things to memorize into one thing to understand.

Check yourself

4 questions · pass 3/4 to unlock Searching a Component Tree

up to 50
  1. 1.Flattening a nested comment tree of n total comments (including all replies at every depth) into a single flat array, via depth-first traversal, has what time and space complexity?

  2. 2.Building a nested file tree from a flat list of n paths (like ['/a/b.txt', '/a/c/d.txt']) by processing one path at a time and walking/creating folder nodes as needed, generally costs how much per path, and why?

  3. 3.A flat-list-to-tree builder uses a Map keyed by each node's id to find a node's parent in O(1), rather than searching the partially-built tree for it. Why does this matter for the overall complexity?

  4. 4.What is the essential relationship between 'flatten a nested comment tree' and 'build a nested tree from a flat list'?

4 left to answer