Nested Comment Widget
Challenge: "Design a comment widget like Reddit or Facebook â support nested replies, edit/delete, reactions, and collapse threads."
Requirements
Functional
- Add top-level comments
- Reply to any comment (unlimited nesting)
- Edit and delete own comments
- Like/dislike reactions
- Collapse/expand reply threads
- Load more replies (pagination per thread)
- Sort: Newest, Oldest, Most Liked
Non-Functional
- Optimistic updates for instant feedback
- Handle threads with 1000+ comments
- Accessible nested tree navigation
- Real-time new comment notifications
Data Model
interface Comment {
id: string;
parentId: string | null;
content: string;
author: { id: string; name: string; avatar: string };
createdAt: string;
updatedAt?: string;
likes: number;
dislikes: number;
userVote: 'like' | 'dislike' | null;
replyCount: number;
isDeleted: boolean;
}
type CommentTree = Comment & { replies: CommentTree[] };Flat vs Tree Storage
API returns flat list with parentId pointers (efficient for the server):
[
{ "id": "1", "parentId": null, "content": "Great article!" },
{ "id": "2", "parentId": "1", "content": "Thanks!" },
{ "id": "3", "parentId": "1", "content": "Agreed." },
{ "id": "4", "parentId": "2", "content": "No problem!" }
]Client builds the tree:
function buildTree(comments: Comment[]): CommentTree[] {
const map = new Map<string, CommentTree>();
const roots: CommentTree[] = [];
for (const comment of comments) {
map.set(comment.id, { ...comment, replies: [] });
}
for (const comment of comments) {
const node = map.get(comment.id)!;
if (comment.parentId) {
map.get(comment.parentId)?.replies.push(node);
} else {
roots.push(node);
}
}
return roots;
}Component Architecture
CommentSection
âââ CommentForm (top-level)
âââ SortControls
âââ CommentList
âââ CommentItem
â âââ Avatar + Author + Timestamp
â âââ Content (or [deleted])
â âââ ActionBar (Like, Dislike, Reply, Edit, Delete)
â âââ CommentForm (inline reply, shown on "Reply" click)
â âââ CommentList (recursive â nested replies)
â âââ CommentItem
â â âââ ...
â âââ "Load more replies" button
âââ "Load more comments" buttonRecursive Comment Rendering
function CommentItem({
comment,
depth = 0,
maxDepth = 8,
}: {
comment: CommentTree;
depth?: number;
maxDepth?: number;
}) {
const [isReplying, setIsReplying] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [collapsed, setCollapsed] = useState(false);
return (
<div
className="comment"
style={{ marginLeft: Math.min(depth * 24, maxDepth * 24) }}
role="article"
aria-label={`Comment by ${comment.author.name}`}
>
<div className="comment-header">
<img src={comment.author.avatar} alt="" className="avatar" />
<span className="author">{comment.author.name}</span>
<time>{formatRelative(comment.createdAt)}</time>
{comment.replies.length > 0 && (
<button
onClick={() => setCollapsed(!collapsed)}
aria-expanded={!collapsed}
>
{collapsed ? `âķ ${comment.replies.length} replies` : 'âž'}
</button>
)}
</div>
{comment.isDeleted ? (
<p className="deleted">[Comment deleted]</p>
) : isEditing ? (
<EditForm comment={comment} onCancel={() => setIsEditing(false)} />
) : (
<p className="content">{comment.content}</p>
)}
<ActionBar
comment={comment}
onReply={() => setIsReplying(true)}
onEdit={() => setIsEditing(true)}
/>
{isReplying && (
<CommentForm
parentId={comment.id}
onSubmit={() => setIsReplying(false)}
onCancel={() => setIsReplying(false)}
autoFocus
/>
)}
{!collapsed && comment.replies.length > 0 && (
<div className="replies" role="list">
{comment.replies.map(reply => (
<CommentItem
key={reply.id}
comment={reply}
depth={depth + 1}
maxDepth={maxDepth}
/>
))}
</div>
)}
</div>
);
}Key Design Decisions
1. Pagination Strategy
Top-level: cursor-based pagination (load more at bottom) Nested replies: fetch on expand (don't load all nested replies upfront)
async function loadReplies(parentId: string, cursor?: string) {
const res = await fetch(
`/api/comments/${parentId}/replies?cursor=${cursor}&limit=10`
);
return res.json();
}2. Optimistic Updates
function addComment(content: string, parentId: string | null) {
const tempId = `temp-${Date.now()}`;
const optimistic: Comment = {
id: tempId,
parentId,
content,
author: currentUser,
createdAt: new Date().toISOString(),
likes: 0,
dislikes: 0,
userVote: null,
replyCount: 0,
isDeleted: false,
};
dispatch({ type: 'ADD_OPTIMISTIC', comment: optimistic });
api.createComment({ content, parentId })
.then(real => dispatch({ type: 'REPLACE_TEMP', tempId, comment: real }))
.catch(() => dispatch({ type: 'REMOVE_TEMP', tempId }));
}3. Deep Nesting Handling
After maxDepth levels, stop indenting and show a "Continue thread â" link that opens a focused view of that sub-thread.
4. Soft Delete
Deleted comments with replies are marked isDeleted: true (content hidden) but kept in the tree to preserve thread structure. Leaf deletions remove entirely.
Performance Considerations
- Virtualization: For 1000+ comments, virtualize the visible list
- Memo:
React.memoonCommentItemâ most re-renders only affect one comment - Batch reactions: Debounce like/dislike API calls
- Lazy avatar loading:
loading="lazy"on avatar images below fold