Lesson 20 of 30
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.
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.
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
Mapby 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
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.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.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.What is the essential relationship between 'flatten a nested comment tree' and 'build a nested tree from a flat list'?
4 left to answer