A threaded comment section supporting arbitrary reply depth, collapsing, and inline editing.
Illustration coming soon
HOW TO USE THIS CHALLENGE
1. Read the briefClarify decisions before coding.
2. Build from memoryUse the 65-minute target.
3. Study the guideCompare architecture, tests, and trade-offs.
REQUIREMENTS
Render comments as a tree: each comment can have any number of replies, at any depth.
Reply to any comment via an inline form; new replies appear nested under their parent immediately.
Collapse/expand a thread, hiding its descendants and showing a reply count.
Edit and delete a comment the current user authored.
Sort top-level threads by newest or most replies.
EDGE CASES
Deleting a comment with existing replies: replace its content with a 'deleted' placeholder rather than removing the whole subtree.
Very deep threads should cap visual indentation (flattening further nesting) so the layout doesn't break on mobile.
Submitting an empty reply should be prevented client-side with a clear message.
Optimistically added replies must reconcile correctly if the server ultimately returns a different id.
ACCESSIBILITY
Each comment's reply/edit/delete controls are reachable via keyboard in a sensible tab order relative to nesting.
Collapse/expand toggles use aria-expanded and a clear accessible name (e.g. 'Hide 4 replies').
Focus moves to the new reply's textarea when the reply form opens.
SUGGESTED APPROACH
Model as a flat map of comments keyed by id, each storing its parentId, plus a derived tree built once for rendering — flat storage makes edit/delete/insert O(1) lookups instead of recursive tree surgery.
Render recursively: a Comment component renders its own body then maps its children ids through itself.
Handle 'reply' by inserting a new node into the flat map with the right parentId and re-deriving the affected branch of the tree.
Cap rendered indentation at a maximum depth constant, continuing to nest logically past that point without visually compounding margin.
EVALUATION RUBRIC
Arbitrary-depth nesting renders and updates correctly (add/edit/delete) without breaking sibling threads.
Deleted comments with replies preserve the subtree via a placeholder.
Collapse/expand state is scoped per-thread and doesn't leak across threads.
Deep threads remain usable on a narrow viewport.
01Understand the product before coding
Learning goals
Render comments as a tree: each comment can have any number of replies, at any depth.
Reply to any comment via an inline form; new replies appear nested under their parent immediately.
Collapse/expand a thread, hiding its descendants and showing a reply count.
Edit and delete a comment the current user authored.
Decisions to state aloud
Deleting a comment with existing replies: replace its content with a 'deleted' placeholder rather than removing the whole subtree.
Very deep threads should cap visual indentation (flattening further nesting) so the layout doesn't break on mobile.
Submitting an empty reply should be prevented client-side with a clear message.
Optimistically added replies must reconcile correctly if the server ultimately returns a different id.
02State model and invariants
Store comments in a normalized map with parentId and ordered childIds. Collapse, selection, and editor drafts are UI state keyed by id. A deleted parent becomes a tombstone when descendants exist, preserving reply structure.
Create a client id and pending comment immediately. On success replace the record key and every parent child reference in one transaction; on failure retain the draft with a retryable failed status.
TypeScript
1functionreplaceId(state: State, tempId:string, saved: Comment){2const temp = state.comments[tempId];3const parent = state.comments[temp.parentId!];4const childIds = parent.childIds.map((id)=> id === tempId ? saved.id : id);5// replace record and parent together; never append a second copy6returnpatchRecords(state, tempId, saved,{...parent, childIds });7}
Step 3
Preserve descendants on delete
If a comment has children, replace body and author presentation with a deleted tombstone. Remove a leaf only after choosing the next logical focus destination.
Step 4
Cap visual indentation
Logical nesting remains in the DOM, but CSS indentation stops after a small depth and a continuation treatment preserves readability on mobile.
SOLComplete solution, explained simply
Build it yourself first. This is one correct implementation, not the only one — read it top to bottom, then compare the shape of your version.
We are building a comment thread where you can reply at any depth, the reply shows instantly before the server confirms, a failed reply can be retried, and deleting a comment that has replies keeps the replies. The one big idea: store every comment flat by id with a list of child ids, render it recursively, and treat the temporary id you invent for an optimistic reply as something you later swap for the real one in exactly one place.
state.comments is { id -> comment } and each comment holds childIds, an ordered list of its direct replies. rootIds is the top-level order. Adding or editing one comment is a targeted object update, not a rebuild of a deep tree.
Node renders itself and then its children
The Node component draws one comment, then maps over childIds and renders a Node for each, one level deeper. Recursion here is safe because the data is a real tree (every child has exactly one parent) and depth only feeds an indentation number.
An optimistic reply uses a temporary id
When you post, we invent a temp- id and add the comment immediately with status 'pending', so it appears at once. The real save runs in the background. This temp id is a placeholder we will replace.
'saved' swaps the temp id in exactly one place
When the server returns the real comment, the reducer deletes the temp record, adds the real one (carrying over any childIds the temp already collected), and repoints the parent's childIds (or rootIds) entry from the temp id to the real id — one atomic update, so a second copy is never appended.
Failure keeps the draft and offers Retry
On error the comment flips to status 'failed' and stays on screen with its text. Retry calls post again with the same existing id, which dispatches 'retry' (back to pending) and re-sends — it does not create another comment.
Delete with replies leaves a tombstone
If the comment has children, deleting it just blanks the author and body and sets status 'deleted', so the replies underneath stay connected. Only a comment with no replies is actually removed and unlinked from its parent.
Indentation is capped
indent = Math.min(depth, MAX_INDENT) * 20, so after five levels the visual nesting stops growing. The DOM hierarchy and any accessible labels still reflect the true depth; only the left margin is clamped so deep threads stay readable on a phone.
Why this is correct
Flat id-keyed storage with childIds lists keeps edits and optimistic swaps to small, targeted updates.
The temporary id for an optimistic reply must be replaced in one atomic reducer step that also fixes the parent reference, or you get a duplicate.
A failed reply stays visible with its text and retries in place using the same id; it never spawns a second comment.
Deleting a comment that has replies converts it to a tombstone so the replies are not orphaned.
Visual indentation is clamped past a small depth while the logical tree stays intact.
05Testing strategy
Critical behavior
Arbitrary-depth nesting renders and updates correctly (add/edit/delete) without breaking sibling threads.
Deleted comments with replies preserve the subtree via a placeholder.
Collapse/expand state is scoped per-thread and doesn't leak across threads.
Deep threads remain usable on a narrow viewport.
Failure and boundary cases
Deleting a comment with existing replies: replace its content with a 'deleted' placeholder rather than removing the whole subtree.
Very deep threads should cap visual indentation (flattening further nesting) so the layout doesn't break on mobile.
Submitting an empty reply should be prevented client-side with a clear message.
Optimistically added replies must reconcile correctly if the server ultimately returns a different id.
Accessibility
Each comment's reply/edit/delete controls are reachable via keyboard in a sensible tab order relative to nesting.
Collapse/expand toggles use aria-expanded and a clear accessible name (e.g. 'Hide 4 replies').
Focus moves to the new reply's textarea when the reply form opens.
06Performance and production hardening
Paginate top-level threads and lazy-load large reply branches.
Memoize nodes by normalized record identity.
Maintain descendant counts instead of walking huge subtrees every render.
Virtualize only top-level or flattened visible rows with stable focus restoration.
QAInterview questions and model answers
Answer aloud first. Then open the model answer and compare state ownership, failure handling, accessibility, and trade-offs—not exact wording.
Model answer
Edits and optimistic reconciliation become direct record updates while parent-child order remains explicit and recursive rendering stays simple.