Part V β The Craft Β· Lesson 10.9
Nested Comments
In one lineA recursive tree render over a flat, normalized store β with a depth cap so it stays usable.
Where you've seen itA Reddit thread where the fourteenth reply is two words wide.
Diagram
STORE FLAT, RENDER AS A TREE
API / store (normalized β 6.6)
ββββββββββββββββββββββββββββββββββββββββββββ
β c1: { id:'c1', parentId: null, text:β¦ } β
β c2: { id:'c2', parentId: 'c1', text:β¦ } β
β c3: { id:'c3', parentId: 'c1', text:β¦ } β
β c4: { id:'c4', parentId: 'c2', text:β¦ } β
ββββββββββββββββββββββββββββββββββββββββββββ
β build once, memoised
βΌ
c1
βββ c2
β βββ c4
βββ c3
Flat storage: O(1) updates by id, no deep cloning.
Tree derived at render time.RECURSIVE RENDER β the component calls itself
<Comment id="c1">
ββ body + actions
ββ children.map(id => <Comment id={id} depth={depth+1} />)
β
ββ base case: no children β stop
ββ depth cap: > 5 β "Continue thread β"DEPTH CAP β why it isn't optional
depth 0 ββββββββββββββββββββββββββββββββββββ 100%
depth 3 ββββββββββββββββββββββββββ 76%
depth 6 ββββββββββββββββ 52%
depth 9 βββββββ 28%
depth 12 ββ 8% β unreadable on mobile
After ~5 levels: stop indenting, or link to a focused sub-thread view.How it works
- Store flat, keyed by id, with a
parentIdon each node β updates and inserts stay O(1). - Build the children map once (
parentId β [childIds]) and memoise it; rebuilding per render is the usual performance bug. - Render recursively with a
depthprop, capping indentation and offering "continue thread" beyond the cap. - Collapse subtrees by storing a set of collapsed ids β collapsing must hide the whole subtree, not just direct children.
- Lazy-load deep replies. Fetch top-level comments with a couple of levels, then load more on demand.
const childrenOf = useMemo(() => {
const map = new Map();
for (const c of Object.values(byId))
map.set(c.parentId, [...(map.get(c.parentId) ?? []), c.id]);
return map;
}, [byId]);
function Comment({ id, depth = 0 }) {
const kids = childrenOf.get(id) ?? [];
if (depth > MAX_DEPTH) return <a href={`/thread/${id}`}>Continue thread β</a>;
return (
<li style={{ '--depth': depth }}>
<Body id={id} />
{kids.length > 0 && (
<ul>{kids.map((k) => <Comment key={k} id={k} depth={depth + 1} />)}</ul>
)}
</li>
);
}Trade-offs
| Recursive render | Flattened render |
|---|---|
| Mirrors the data, simple to read | Easier to virtualize |
| Deep trees risk stack/perf issues | Manual indent bookkeeping |
| Natural collapse semantics | Better for thousands of nodes |
Accessibility: nested <ul>/<li> gives screen readers real depth information for free. Collapse toggles follow the accordion contract from 10.8, and each comment should be reachable by keyboard without tabbing through every action button β consider a roving tabindex (8.2).
Interview angle
"Build Reddit-style nested comments."
- Normalized flat store, memoised parentβchildren map, recursive component with a depth prop.
- Cap indentation around five levels and link deeper threads to their own view β this is a UX requirement, not a shortcut.
- For very large threads, flatten to a visible-nodes array and virtualize; recursion and virtualization don't compose easily.
Recap
- Flat, normalized storage; tree derived and memoised at render.
- Recursion needs a base case and a depth cap.
- Collapse hides the subtree; nested lists give a11y depth for free.