Learn how to easily add commenting to your web app with this step-by-step guide for better user engagement and feedback.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Why Comments Matter for Your Business
Comments aren't just a technical feature—they're a business multiplier. They increase user engagement by 5-10x, provide valuable feedback loops, and create community around your product. A well-designed commenting system transforms passive visitors into active participants and loyal users.
Three Implementation Approaches
Let's explore each implementation path with practical code examples:
Database Structure
CREATE TABLE comments (
id INT PRIMARY KEY AUTO_INCREMENT,
content TEXT NOT NULL,
user_id INT NOT NULL,
post_id INT NOT NULL,
parent_id INT NULL, -- For threaded comments
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (post_id) REFERENCES posts(id),
FOREIGN KEY (parent_id) REFERENCES comments(id)
);
-- Consider adding indexes for performance
CREATE INDEX idx_post_id ON comments(post_id);
CREATE INDEX idx_parent_id ON comments(parent_id);
Backend API (Node.js/Express Example)
// comments.js - RESTful API routes for comments
const express = require('express');
const router = express.Router();
const db = require('../database');
const auth = require('../middleware/auth');
// Get comments for a post
router.get('/posts/:postId/comments', async (req, res) => {
try {
const comments = await db.query(
`SELECT c.*, u.username, u.avatar
FROM comments c
JOIN users u ON c.user_id = u.id
WHERE c.post_id = ?
ORDER BY c.created_at DESC`,
[req.params.postId]
);
// Transform flat comments into threaded structure
const threaded = comments.reduce((acc, comment) => {
if (!comment.parent_id) {
comment.replies = [];
acc[comment.id] = comment;
} else if (acc[comment.parent_id]) {
acc[comment.parent_id].replies.push(comment);
}
return acc;
}, {});
res.json(Object.values(threaded));
} catch (error) {
console.error('Error fetching comments:', error);
res.status(500).json({ error: 'Failed to load comments' });
}
});
// Post a new comment
router.post('/posts/:postId/comments', auth.required, async (req, res) => {
try {
const { content, parentId } = req.body;
// Validate input
if (!content || content.trim() === '') {
return res.status(400).json({ error: 'Comment cannot be empty' });
}
// Check for spam/inappropriate content
if (containsInappropriateContent(content)) {
return res.status(400).json({ error: 'Comment contains inappropriate content' });
}
const result = await db.query(
`INSERT INTO comments (content, user_id, post_id, parent_id)
VALUES (?, ?, ?, ?)`,
[content, req.user.id, req.params.postId, parentId || null]
);
// Return the new comment with user info
const newComment = await db.query(
`SELECT c.*, u.username, u.avatar
FROM comments c
JOIN users u ON c.user_id = u.id
WHERE c.id = ?`,
[result.insertId]
);
res.status(201).json(newComment[0]);
} catch (error) {
console.error('Error posting comment:', error);
res.status(500).json({ error: 'Failed to post comment' });
}
});
// Other routes for updating, deleting comments...
function containsInappropriateContent(text) {
// Implement content moderation logic here
// Could use regex for basic filtering or call external API
const badWords = ['badword1', 'badword2']; // Simplified example
return badWords.some(word => text.toLowerCase().includes(word));
}
module.exports = router;
Frontend Implementation (React)
// CommentSection.jsx
import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';
import CommentForm from './CommentForm';
import Comment from './Comment';
import './CommentSection.css';
const CommentSection = ({ postId }) => {
const [comments, setComments] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const { user } = useAuth();
useEffect(() => {
const fetchComments = async () => {
try {
setIsLoading(true);
const response = await fetch(`/api/posts/${postId}/comments`);
if (!response.ok) {
throw new Error('Failed to fetch comments');
}
const data = await response.json();
setComments(data);
} catch (err) {
setError(err.message);
console.error('Error loading comments:', err);
} finally {
setIsLoading(false);
}
};
fetchComments();
// Optional: Set up real-time updates with WebSocket
const socket = new WebSocket(`ws://${window.location.host}/comments`);
socket.onmessage = (event) => {
const newComment = JSON.parse(event.data);
if (newComment.post_id === postId) {
setComments(prevComments => addCommentToThread(prevComments, newComment));
}
};
return () => socket.close();
}, [postId]);
const addCommentToThread = (comments, newComment) => {
// Logic to add a new comment to the threaded structure
if (!newComment.parent_id) {
return [...comments, {...newComment, replies: []}];
}
return comments.map(comment => {
if (comment.id === newComment.parent_id) {
return {
...comment,
replies: [...comment.replies, newComment]
};
}
return comment;
});
};
const handleAddComment = async (content, parentId = null) => {
try {
const response = await fetch(`/api/posts/${postId}/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${user.token}`
},
body: JSON.stringify({ content, parentId })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to post comment');
}
const newComment = await response.json();
setComments(prevComments => addCommentToThread(prevComments, newComment));
} catch (err) {
console.error('Error posting comment:', err);
// Show error to user
}
};
if (isLoading) return <div className="comments-loading">Loading comments...</div>;
if (error) return <div className="comments-error">Error: {error}</div>;
return (
<div className="comment-section">
<h3>{comments.length} Comments</h3>
{user ? (
<CommentForm onSubmit={content => handleAddComment(content)} />
) : (
<p className="login-prompt">Please <a href="/login">sign in</a> to leave a comment.</p>
)}
<div className="comments-list">
{comments.length === 0 ? (
<p className="no-comments">Be the first to comment!</p>
) : (
comments.map(comment => (
<Comment
key={comment.id}
comment={comment}
onReply={handleAddComment}
currentUser={user}
/>
))
)}
</div>
</div>
);
};
export default CommentSection;
// Comment.jsx
import React, { useState } from 'react';
import CommentForm from './CommentForm';
import TimeAgo from './TimeAgo';
import './Comment.css';
const Comment = ({ comment, onReply, currentUser }) => {
const [showReplyForm, setShowReplyForm] = useState(false);
const handleReply = (content) => {
onReply(content, comment.id);
setShowReplyForm(false);
};
return (
<div className="comment">
<div className="comment-header">
<img
src={comment.avatar || '/default-avatar.png'}
alt={comment.username}
className="comment-avatar"
/>
<div className="comment-meta">
<span className="comment-author">{comment.username}</span>
<TimeAgo date={comment.created_at} />
</div>
</div>
<div className="comment-content">{comment.content}</div>
<div className="comment-actions">
{currentUser && (
<button
className="reply-button"
onClick={() => setShowReplyForm(!showReplyForm)}
>
{showReplyForm ? 'Cancel' : 'Reply'}
</button>
)}
</div>
{showReplyForm && (
<CommentForm
onSubmit={handleReply}
placeholder={`Reply to ${comment.username}...`}
buttonText="Reply"
/>
)}
{comment.replies && comment.replies.length > 0 && (
<div className="comment-replies">
{comment.replies.map(reply => (
<Comment
key={reply.id}
comment={reply}
onReply={onReply}
currentUser={currentUser}
/>
))}
</div>
)}
</div>
);
};
export default Comment;
Popular Third-Party Options
Implementing Disqus (Example)
// DisqusComments.jsx
import React from 'react';
import { DiscussionEmbed } from 'disqus-react';
const DisqusComments = ({ post, url }) => {
const disqusConfig = {
url: url,
identifier: `post-${post.id}`,
title: post.title,
};
return (
<div className="comments-section">
<h3>Comments</h3>
<DiscussionEmbed
shortname="your-disqus-shortname"
config={disqusConfig}
/>
</div>
);
};
export default DisqusComments;
Usage in your main component
import DisqusComments from './DisqusComments';
const BlogPost = ({ post }) => {
return (
<div className="blog-post">
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
<DisqusComments
post={post}
url={`https://yourdomain.com/posts/${post.slug}`}
/>
</div>
);
};
Integrating an Open-Source Solution (Example with React Comments)
// Using react-comments-section library
import React, { useState, useEffect } from 'react';
import { CommentSection } from 'react-comments-section';
import 'react-comments-section/dist/index.css';
import { fetchComments, postComment } from '../api/comments';
const CommentsContainer = ({ postId, currentUser }) => {
const [comments, setComments] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadComments = async () => {
try {
setIsLoading(true);
const data = await fetchComments(postId);
// Transform your API's comment format to match the library's expected format
const formattedComments = data.map(comment => ({
userId: comment.user_id,
comId: comment.id,
fullName: comment.username,
avatarUrl: comment.avatar,
text: comment.content,
replies: formatReplies(comment.replies || []),
timeStamp: new Date(comment.created_at).toISOString()
}));
setComments(formattedComments);
} catch (error) {
console.error('Failed to load comments:', error);
} finally {
setIsLoading(false);
}
};
loadComments();
}, [postId]);
const formatReplies = (replies) => {
return replies.map(reply => ({
userId: reply.user_id,
comId: reply.id,
fullName: reply.username,
avatarUrl: reply.avatar,
text: reply.content,
timeStamp: new Date(reply.created_at).toISOString()
}));
};
const handleSubmitComment = async (data) => {
try {
// Transform the library's comment format back to your API format
const apiComment = {
content: data.text,
parent_id: data.parentOfCommentsId || null
};
const savedComment = await postComment(postId, apiComment);
// Update local state with the new comment
// The library will handle this automatically if you return the right format
return {
userId: savedComment.user_id,
comId: savedComment.id,
fullName: savedComment.username,
avatarUrl: savedComment.avatar,
text: savedComment.content,
timeStamp: new Date(savedComment.created_at).toISOString()
};
} catch (error) {
console.error('Failed to post comment:', error);
return null;
}
};
if (isLoading) return <div>Loading comments...</div>;
return (
<div className="comments-container">
<CommentSection
currentUser={currentUser ? {
userId: currentUser.id,
avatarUrl: currentUser.avatar,
name: currentUser.username
} : null}
commentsArray={comments}
onSubmitAction={handleSubmitComment}
logIn={{
loginLink: '/login',
signupLink: '/signup'
}}
customNoComment="Be the first to comment!"
/>
</div>
);
};
export default CommentsContainer;
Server-side setup
// server.js
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const commentsRouter = require('./routes/comments');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
// Socket.IO setup for real-time comments
io.on('connection', (socket) => {
console.log('New client connected');
socket.on('joinRoom', (postId) => {
socket.join(`post-${postId}`);
console.log(`Client joined room: post-${postId}`);
});
socket.on('leaveRoom', (postId) => {
socket.leave(`post-${postId}`);
console.log(`Client left room: post-${postId}`);
});
socket.on('disconnect', () => {
console.log('Client disconnected');
});
});
// Make io accessible to our routes
app.use((req, res, next) => {
req.io = io;
next();
});
app.use('/api', commentsRouter);
server.listen(3000, () => {
console.log('Server running on port 3000');
});
Updated route to broadcast new comments
// Modify the POST endpoint in comments.js
router.post('/posts/:postId/comments', auth.required, async (req, res) => {
try {
// ... existing code to save comment ...
// Broadcast the new comment to all clients in the post's room
req.io.to(`post-${req.params.postId}`).emit('newComment', newComment);
res.status(201).json(newComment);
} catch (error) {
console.error('Error posting comment:', error);
res.status(500).json({ error: 'Failed to post comment' });
}
});
Updated frontend to receive real-time comments
// Modified CommentSection.jsx with Socket.IO
import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { io } from 'socket.io-client';
import CommentForm from './CommentForm';
import Comment from './Comment';
const CommentSection = ({ postId }) => {
const [comments, setComments] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [socket, setSocket] = useState(null);
const { user } = useAuth();
// Initialize Socket.IO connection
useEffect(() => {
const newSocket = io();
setSocket(newSocket);
// Join the room for this post
newSocket.emit('joinRoom', postId);
// Listen for new comments
newSocket.on('newComment', (newComment) => {
setComments(prevComments => addCommentToThread(prevComments, newComment));
});
return () => {
newSocket.emit('leaveRoom', postId);
newSocket.disconnect();
};
}, [postId]);
// Fetch initial comments
useEffect(() => {
const fetchComments = async () => {
// ... existing code to fetch comments ...
};
fetchComments();
}, [postId]);
// ... rest of the component remains the same ...
};
Decision Framework
Key Business Considerations
Remember, a good commenting system isn't just a technical feature—it's a business asset that creates community, provides valuable feedback, and keeps users engaged with your product. Choose and implement your solution with these strategic goals in mind.
Explore the top 3 ways commenting boosts engagement and collaboration in your web app.
Preserving critical decision context directly within code
Accelerating developer ramp-up and reducing bus factor risks
Reducing the long-term cost of ownership for critical codebases
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â