We use cookies to enhance your experience on the site
CodeWorlds

Threaded Comments - Nested Replies

Reddit has threaded comments - comments can have replies, which in turn have their own replies. It's a hierarchical tree structure.

Data Structure - Recursive

1interface Comment {
2  id: number;
3  author: string;
4  content: string;
5  upvotes: number;
6  downvotes: number;
7  userVote: 'up' | 'down' | null;
8  timestamp: Date;
9  replies: Comment[]; // 🔑 Recursion - comments contain comments!
10}

Key concept:

replies
is an array of comments that can themselves have
replies
.

Example data:

1const exampleComment: Comment = {
2  id: 1,
3  author: 'Redbeard',
4  content: 'I found a map to the treasure island!',
5  upvotes: 42,
6  downvotes: 3,
7  userVote: null,
8  timestamp: new Date(),
9  replies: [
10    {
11      id: 2,
12      author: 'Morgan',
13      content: 'Where exactly?',
14      upvotes: 15,
15      downvotes: 0,
16      userVote: null,
17      timestamp: new Date(),
18      replies: [
19        {
20          id: 3,
21          author: 'Redbeard',
22          content: 'North of Tortuga!',
23          upvotes: 8,
24          downvotes: 0,
25          userVote: null,
26          timestamp: new Date(),
27          replies: [], // No further replies
28        },
29      ],
30    },
31  ],
32};

Visualization:

1Comment 1 (Redbeard)
2└── Comment 2 (Morgan)
3    └── Comment 3 (Redbeard)

Recursive Component - Rendering the Tree

To display nested comments, the component must render itself:

1'use client';
2
3import { useState } from 'react';
4import VoteButtons from './VoteButtons';
5
6interface CommentProps {
7  comment: Comment;
8  depth?: number; // Nesting depth (for indentation)
9}
10
11export default function CommentThread({ comment, depth = 0 }: CommentProps) {
12  const [isCollapsed, setIsCollapsed] = useState(false);
13  const [showReplyForm, setShowReplyForm] = useState(false);
14
15  const score = comment.upvotes - comment.downvotes;
16
17  return (
18    <div className={`${depth > 0 ? 'ml-8 border-l-2 border-gray-200 pl-4' : ''}`}>
19      {/* Comment Header */}
20      <div className="flex gap-3 py-3">
21        <VoteButtons
22          initialUpvotes={comment.upvotes}
23          initialDownvotes={comment.downvotes}
24          initialUserVote={comment.userVote}
25        />
26
27        <div className="flex-1">
28          {/* Author & Time */}
29          <div className="flex items-center gap-2 text-xs text-gray-600 mb-1">
30            <span className="font-bold text-gray-900">{comment.author}</span>
31            <span></span>
32            <span>{formatTimeAgo(comment.timestamp)}</span>
33            {isCollapsed && (
34              <span className="text-blue-500">
35                [{comment.replies.length} children]
36              </span>
37            )}
38          </div>
39
40          {/* Content */}
41          {!isCollapsed && (
42            <>
43              <p className="text-gray-800 text-sm mb-2">{comment.content}</p>
44
45              {/* Actions */}
46              <div className="flex items-center gap-3 text-xs text-gray-500">
47                <button
48                  onClick={() => setShowReplyForm(!showReplyForm)}
49                  className="hover:underline"
50                >
51                  💬 Reply
52                </button>
53                <button className="hover:underline">🔗 Share</button>
54                <button className="hover:underline">🏴 Report</button>
55                {comment.replies.length > 0 && (
56                  <button
57                    onClick={() => setIsCollapsed(true)}
58                    className="hover:underline"
59                  >
60                    [] Collapse
61                  </button>
62                )}
63              </div>
64
65              {/* Reply Form */}
66              {showReplyForm && (
67                <div className="mt-3 bg-gray-50 p-3 rounded">
68                  <textarea
69                    placeholder="Write your reply..."
70                    className="w-full p-2 border rounded text-sm"
71                    rows={3}
72                  />
73                  <div className="flex gap-2 mt-2">
74                    <button className="px-3 py-1 bg-blue-500 text-white rounded text-sm">
75                      Reply
76                    </button>
77                    <button
78                      onClick={() => setShowReplyForm(false)}
79                      className="px-3 py-1 bg-gray-200 rounded text-sm"
80                    >
81                      Cancel
82                    </button>
83                  </div>
84                </div>
85              )}
86            </>
87          )}
88
89          {/* Collapsed State */}
90          {isCollapsed && (
91            <button
92              onClick={() => setIsCollapsed(false)}
93              className="text-xs text-blue-500 hover:underline"
94            >
95              [+] Expand
96            </button>
97          )}
98        </div>
99      </div>
100
101      {/* 🔑 RECURSION - Render replies using the same component */}
102      {!isCollapsed && comment.replies.length > 0 && (
103        <div className="mt-2">
104          {comment.replies.map(reply => (
105            <CommentThread
106              key={reply.id}
107              comment={reply}
108              depth={depth + 1} // Increase depth for indentation
109            />
110          ))}
111        </div>
112      )}
113    </div>
114  );
115}

Depth Limiting - Limiting Nesting Depth

Reddit limits depth to ~10 levels and adds "Continue this thread →":

1const MAX_DEPTH = 10;
2
3export default function CommentThread({ comment, depth = 0 }: CommentProps) {
4  // ... code
5  // If depth is too large, show a link instead of rendering replies
6  if (depth >= MAX_DEPTH && comment.replies.length > 0) {
7    return (
8      <div className="ml-8 pl-4 border-l-2 border-gray-200">
9        <a
10          href={`/comment/${comment.id}`}
11          className="text-blue-500 text-sm hover:underline"
12        >
13          Continue this thread →
14        </a>
15      </div>
16    );
17  }
18
19  // Rest of the code...
20}

Sorting Replies

Comments can also be sorted:

1function sortComments(comments: Comment[], sortMode: 'best' | 'new' | 'top'): Comment[] {
2  const sorted = [...comments];
3
4  switch (sortMode) {
5    case 'best':
6      // Wilson score algorithm (simplified)
7      sorted.sort((a, b) => {
8        const scoreA = a.upvotes - a.downvotes;
9        const scoreB = b.upvotes - b.downvotes;
10        return scoreB - scoreA;
11      });
12      break;
13    case 'new':
14      sorted.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
15      break;
16    case 'top':
17      sorted.sort((a, b) => {
18        const scoreA = a.upvotes - a.downvotes;
19        const scoreB = b.upvotes - b.downvotes;
20        return scoreB - scoreA;
21      });
22      break;
23  }
24
25  // Recursively sort replies
26  return sorted.map(comment => ({
27    ...comment,
28    replies: sortComments(comment.replies, sortMode),
29  }));
30}

Adding Replies - Adding Responses

To add a reply to a specific comment in the tree, we need a recursive function:

1function addReplyToComment(
2  comments: Comment[],
3  parentId: number,
4  newReply: Comment
5): Comment[] {
6  return comments.map(comment => {
7    if (comment.id === parentId) {
8      // Found the parent - add the reply
9      return {
10        ...comment,
11        replies: [...comment.replies, newReply],
12      };
13    }
14
15    // Search deeper in replies
16    return {
17      ...comment,
18      replies: addReplyToComment(comment.replies, parentId, newReply),
19    };
20  });
21}
22
23// Usage:
24const [comments, setComments] = useState<Comment[]>(initialComments);
25
26const handleAddReply = (parentId: number, content: string) => {
27  const newReply: Comment = {
28    id: Date.now(),
29    author: 'You',
30    content,
31    upvotes: 1,
32    downvotes: 0,
33    userVote: null,
34    timestamp: new Date(),
35    replies: [],
36  };
37
38  setComments(addReplyToComment(comments, parentId, newReply));
39};

Comment Count - Counting All Comments

Recursive function to count comments including replies:

1function countTotalComments(comments: Comment[]): number {
2  return comments.reduce((total, comment) => {
3    return total + 1 + countTotalComments(comment.replies);
4  }, 0);
5}
6
7// Example:
8const total = countTotalComments(post.comments);
9// "142 comments"

Highlighting New Comments

Reddit highlights new comments (since last visit):

1interface Comment {
2  // ... other fields
3  isNew?: boolean; // Added since last visit
4}
5
6// In component:
7<div className={`${comment.isNew ? 'bg-blue-50 border-l-4 border-l-blue-500' : ''}`}>
8  {/* Comment content */}
9</div>

Summary 🎓

Recursive data - Comment contains replies: Comment[] ✅ Recursive component - renders itself for replies ✅ Depth tracking - indentation based on depth ✅ Collapse/Expand - collapsing threads ✅ Depth limiting - "Continue thread" for deep threads ✅ Sorting replies - recursive sorting ✅ Adding replies - recursive update function ✅ Comment count - recursive counting ✅ Visual hierarchy - border-l, margin-left

In the next exercise we'll create Communities (subreddits)!

See you there! 🚀

Go to CodeWorlds