/web-app-features

How to Add Commenting to Your Web App

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

Book a free  consultation
4.9
Clutch rating 🌟
600+
Happy partners
17+
Countries served
190+
Team members
Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

How to Add Commenting to Your Web App

Building a Robust Commenting System for Your Web Application

 

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.

 

Architecture Options: Choose Your Path

 

Three Implementation Approaches

 

  • Build from scratch: Complete control but highest development investment
  • Use a third-party service: Quick to implement but less customizable
  • Leverage open-source libraries: Balance of control and development speed

 

Let's explore each implementation path with practical code examples:

 

Option 1: Building Your Own Comment System

 

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;

 

Option 2: Using a Third-Party Commenting Service

 

Popular Third-Party Options

 

  • Disqus: Most widely used, easy to implement
  • Commento: Privacy-focused, lightweight alternative
  • Facebook Comments: Social integration, familiar to users

 

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>
  );
};

 

Option 3: Using Open-Source Libraries

 

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;

 

Advanced Features to Consider

 

  • Real-time updates using WebSockets or Server-Sent Events
  • Content moderation with automated filtering and reporting
  • Rich text formatting with Markdown or WYSIWYG editors
  • Threaded conversations for better discussion organization
  • Upvoting/downvoting to highlight valuable contributions
  • User mentions and notifications to increase engagement

 

Implementation of Real-Time Comments with Socket.IO

 

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 ...
};

 

Choosing the Right Solution for Your Business

 

Decision Framework

 

  • Custom solution: Choose when you need deep integration with your app, unique features, or complete data control
  • Third-party service: Ideal when you need quick implementation, low maintenance, and can accept some limitations
  • Open-source libraries: Best when you want to balance customization with development speed

 

Key Business Considerations

 

  • Development resources: Custom solutions require 80-120 hours of initial development plus ongoing maintenance
  • Data ownership: Third-party services may limit your access to valuable user engagement data
  • User experience: Consider whether requiring additional logins (with third-party services) will affect engagement
  • Scalability: Custom solutions need careful architecture to handle high-traffic sites

 

Implementation Roadmap

 

  1. Start small: Implement basic commenting with minimal features
  2. Gather user feedback: Learn what your specific audience values most
  3. Iterate thoughtfully: Add advanced features based on actual usage patterns
  4. Monitor performance: Comments can become a performance bottleneck if not optimized

 

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.

Ship Commenting 10x Faster with RapidDev

Connect with our team to unlock the full potential of code solutions with a no-commitment consultation!

Book a Free Consultation

Top 3 Commenting Usecases

Explore the top 3 ways commenting boosts engagement and collaboration in your web app.

 

Contextual Documentation

 

Preserving critical decision context directly within code

 

  • Comments serve as institutional memory for why complex implementations exist, explaining the reasoning behind non-obvious solutions that may otherwise appear inefficient to future developers.
  • Prevents the "don't touch this code" syndrome by documenting the business constraints or edge cases that necessitated specific approaches, reducing the risk of future refactoring disasters.
  • Particularly valuable for explaining integration with third-party services, historical workarounds, or compliance requirements that influenced technical decisions.

 

Knowledge Transfer & Onboarding

 

Accelerating developer ramp-up and reducing bus factor risks

 

  • Well-commented code can reduce onboarding time by 30-40% for new team members by providing navigational cues and architectural insights without requiring extensive documentation reading.
  • Creates a continuous, passive knowledge transfer system that reduces dependency on senior developers for explanations, freeing them to focus on higher-value tasks.
  • Especially critical for complex business logic where the "why" behind implementations isn't obvious from the code alone, such as financial calculations, regulatory compliance, or customer-specific business rules.

 

Future-Proofing & Maintenance

 

Reducing the long-term cost of ownership for critical codebases

 

  • Properly commented code serves as a maintenance multiplier, making systems up to 4x faster to debug and modify when original authors are unavailable.
  • Comments explaining performance considerations, potential bottlenecks, or resource constraints provide crucial context for future scaling decisions.
  • Strategically commented TODOs or improvement opportunities create a documented technical debt roadmap, helping prioritize future refactoring efforts while acknowledging current implementation limitations.


Recognized by the best

Trusted by 600+ businesses globally

From startups to enterprises and everything in between, see for yourself our incredible impact.

RapidDev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with.

They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

Arkady
CPO, Praction
Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost.

He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Donald Muir
Co-Founder, Arc
RapidDev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space.

They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Mat Westergreen-Thorne
Co-CEO, Grantify
RapidDev is an excellent developer for custom-code solutions.

We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Emmanuel Brown
Co-Founder, Church Real Estate Marketplace
Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 

This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Samantha Fekete
Production Manager, Media Production Company
The pSEO strategy executed by RapidDev is clearly driving meaningful results.

Working with RapidDev has delivered measurable, year-over-year growth. Comparing the same period, clicks increased by 129%, impressions grew by 196%, and average position improved by 14.6%. Most importantly, qualified contact form submissions rose 350%, excluding spam.

Appreciation as well to Matt Graham for championing the collaboration!

Michael W. Hammond
Principal Owner, OCD Tech

We put the rapid in RapidDev

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.Â