Reddit has threaded comments - comments can have replies, which in turn have their own replies. It's a hierarchical tree structure.
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.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)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}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}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}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};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"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>✅ 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! 🚀