Building a robust comment section is essential for any dynamic content platform, but managing threaded (nested) replies can be tricky.
In this tutorial, we’ll walk through how to build a fully functional, nested comment system using React functional components and hooks.
We will focus on the front-end logic, using a flat array of data and the parentId field to create the recursive structure.
1. Defining the Comment Data Model
The most critical part of a nested comment system is the data structure. We use a flat array where each comment object contains a parentId.
parentId: null: This is a top-level comment.parentId: [ID]: This is a reply to the comment with that ID.
data.js
Let’s start with our mock data file, which simulates the initial comments fetched from a database.
// data.js
export const initialComments = [
// Top-level comments (parentId: null)
{ id: 1, userId: 'userA', content: 'This is the first top-level comment.', parentId: null, timestamp: Date.now() - 50000 },
{ id: 2, userId: 'userB', content: 'I agree, great article!', parentId: null, timestamp: Date.now() - 40000 },
// Replies
{ id: 3, userId: 'userC', content: 'This is a reply to the first comment.', parentId: 1, timestamp: Date.now() - 30000 },
{ id: 4, userId: 'userA', content: 'My second reply to the first comment.', parentId: 1, timestamp: Date.now() - 20000 },
// Nested reply (A reply to a reply)
{ id: 5, userId: 'userD', content: 'A reply to comment #3 (a nested reply).', parentId: 3, timestamp: Date.now() - 10000 },
];
2. The Reusable Comment Form
The form component is simple and “dumb”—it only handles collecting the input text and passing it up to the parent component via the onSubmit prop.
CommentForm.jsx
import React, { useState } from 'react';
const CommentForm = ({ parentId = null, onSubmit }) => {
const [content, setContent] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (content.trim() === '') return;
// Pass the content back to the parent component
onSubmit(content);
setContent('');
};
return (
<form onSubmit={handleSubmit} style={{ margin: '15px 0', padding: '10px', border: '1px solid #ddd', borderRadius: '4px' }}>
<h4>{parentId ? `Reply to Comment ${parentId}` : 'Post a New Comment'}</h4>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Write your comment..."
rows="3"
style={{ width: '100%', resize: 'vertical', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }}
/>
<button
type="submit"
style={{ padding: '8px 15px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
>
Submit Comment
</button>
</form>
);
};
export default CommentForm;
3. The Recursive Comment Item
This is the most powerful part of the system. The CommentItem component calls itself (CommentItem) to render its replies, creating the nesting effect.
CommentItem.jsx
import React, { useState } from 'react';
import CommentForm from './CommentForm';
const CommentItem = ({ comment, allComments, onAddReply }) => {
const [showReplyForm, setShowReplyForm] = useState(false);
// 1. Find all direct replies to the current comment
const replies = allComments.filter(c => c.parentId === comment.id);
const handleReplySubmit = (content) => {
// Call the centralized function in the main container, passing the parent ID
onAddReply(comment.id, content);
setShowReplyForm(false);
};
return (
<div
style={{
marginLeft: comment.parentId ? '20px' : '0',
borderLeft: comment.parentId ? '2px solid #ccc' : 'none',
paddingLeft: comment.parentId ? '10px' : '0',
marginTop: '15px',
paddingBottom: '10px'
}}
>
<div style={{ backgroundColor: comment.parentId ? '#f9f9f9' : '#fff', padding: '10px', borderRadius: '4px' }}>
<p style={{ margin: '0 0 5px 0' }}>
<strong>{comment.userId}</strong>
<span style={{ color: '#666', fontSize: '0.8em', marginLeft: '10px' }}>
({new Date(comment.timestamp).toLocaleTimeString()})
</span>
</p>
<p style={{ margin: '0 0 10px 0' }}>{comment.content}</p>
{/* Reply button and form toggler */}
<button
onClick={() => setShowReplyForm(!showReplyForm)}
style={{ background: 'none', border: 'none', color: '#007bff', cursor: 'pointer', padding: 0 }}
>
{showReplyForm ? 'Cancel Reply' : 'Reply'}
</button>
</div>
{/* Conditionally render the reply form */}
{showReplyForm && (
<CommentForm parentId={comment.id} onSubmit={handleReplySubmit} />
)}
{/* 2. Recursively render replies */}
{replies.length > 0 && (
<div style={{ marginTop: '10px' }}>
{replies.map(reply => (
<CommentItem
key={reply.id}
comment={reply}
allComments={allComments}
onAddReply={onAddReply}
/>
))}
</div>
)}
</div>
);
};
export default CommentItem;
4. The Main Comment Container
The CommentsContainer manages the state, initializes the data, and provides the central function (addComment) for creating both new comments and replies.
CommentsContainer.jsx
import React, { useState } from 'react';
import CommentItem from './CommentItem';
import CommentForm from './CommentForm';
import { initialComments } from './data';
// Simple counter for generating temporary front-end IDs
let nextId = initialComments.length + 1;
const CommentsContainer = () => {
// Use state to hold all comments. In a real app, you'd fetch this with useEffect.
const [comments, setComments] = useState(initialComments);
const addComment = (parentId, content) => {
// --- 1. Create the new comment object ---
const newComment = {
id: nextId++,
userId: 'currentUser', // Placeholder for the authenticated user
content,
parentId, // This is the ID of the parent comment, or null for top-level
timestamp: Date.now(),
};
// --- 2. Update the state (simulates a successful API response) ---
setComments(prevComments => [...prevComments, newComment]);
// **NOTE ON PRODUCTION:**
// In a real application, this is where you would call your API (e.g., fetch('/api/comments', { method: 'POST', body: JSON.stringify(newComment) })).
// You would typically update the state *after* the server confirms the comment was saved.
};
// Filter and sort to show only top-level comments for the initial list
const topLevelComments = comments
.filter(c => c.parentId === null)
.sort((a, b) => b.timestamp - a.timestamp); // Sort by newest first
return (
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '30px', fontFamily: 'Inter, sans-serif', backgroundColor: '#f4f4f9', borderRadius: '8px', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.05)' }}>
<h1 style={{ borderBottom: '2px solid #ddd', paddingBottom: '10px', marginBottom: '20px', color: '#333' }}>
Article Comments ({comments.length})
</h1>
{/* Main Comment Form (parentId: null) */}
<CommentForm
onSubmit={(content) => addComment(null, content)}
parentId={null}
/>
{/* Render the top-level comments, which recursively render their children */}
<div style={{ marginTop: '30px' }}>
{topLevelComments.map(comment => (
<CommentItem
key={comment.id}
comment={comment}
allComments={comments} // Pass the entire list for lookups
onAddReply={addComment} // Pass the centralized function
/>
))}
{topLevelComments.length === 0 && <p style={{ textAlign: 'center', color: '#666' }}>Be the first to share your thoughts!</p>}
</div>
</div>
);
};
export default CommentsContainer;
Useful links below:
Let me & my team build you a money making website/blog for your business https://bit.ly/tnrwebsite_service
Get Bluehost hosting for as little as $1.99/month (save 75%)…https://bit.ly/3C1fZd2
Best email marketing automation solution on the market! http://www.aweber.com/?373860
Build high converting sales funnels with a few simple clicks of your mouse! https://bit.ly/484YV29
Join my Patreon for one-on-one coaching and help with your coding…https://www.patreon.com/c/TyronneRatcliff
Buy me a coffee https://buymeacoffee.com/tyronneratcliff



