Build a Custom Comment Section

Published: (February 19, 2026 at 09:33 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

Adding a Comment Section to a React App – Without the Bloat

“Every off‑the‑shelf solution either forces its own UI on you, dumps a pile of CSS you never asked for, or locks you into a specific backend.”

What if you could get all the hard logic handled for you and still keep complete control over how everything looks and feels?
That’s the whole point of @hasthiya_/headless-comments-react.

What Is It?

@hasthiya_/headless-comments-react is a headless comment engine for React.
It ships a set of hooks that manage the entire comment‑thread state (adding, replying, editing, deleting, reacting, sorting, …) without rendering a single <div>.
You provide the UI – the library provides the brains.

The separation makes it possible to style the comment section like Reddit, Discord, GitHub, or anything else while re‑using the exact same underlying logic.

Installation

npm install @hasthiya_/headless-comments-react
# or
yarn add @hasthiya_/headless-comments-react

Core Hooks (imported from @hasthiya_/headless-comments-react/headless)

HookResponsibility
useCommentTreeManages the whole comment thread – list of comments + CRUD + reaction methods.
useCommentManages a single comment – reply state, edit state, reaction toggles, author check.
useSortedCommentsProvides sorting (newest, oldest, popular).
formatRelativeTime (from the root package)Turns timestamps into human‑readable strings like “5 minutes ago”.

Full Working Example

Below is a minimal, fully functional comment section that you can copy‑paste into a new React component file (e.g. CommentSection.tsx).
All UI is yours – the hooks take care of the rest.

/* --------------------------------------------------------------
   1️⃣ Types & a mock current user
   -------------------------------------------------------------- */
import type { CommentUser } from '@hasthiya_/headless-comments-react';

const currentUser: CommentUser = {
  id: 'user-1',
  name: 'Jane Doe',
  avatarUrl: 'https://example.com/avatar.jpg',
};

/* --------------------------------------------------------------
   2️⃣ Initialise the comment tree
   -------------------------------------------------------------- */
import { useCommentTree } from '@hasthiya_/headless-comments-react/headless';
import { useState } from 'react';

function CommentSection() {
  const tree = useCommentTree({
    initialComments: [], // ← load your existing comments here
  });

  return ;
}

/* --------------------------------------------------------------
   3️⃣ Single comment UI – uses `useComment`
   -------------------------------------------------------------- */
import {
  useComment,
  useSortedComments,
} from '@hasthiya_/headless-comments-react/headless';
import { formatRelativeTime } from '@hasthiya_/headless-comments-react';
import type {
  Comment,
  UseCommentTreeReturn,
  CommentUser,
} from '@hasthiya_/headless-comments-react';

function CommentItem({
  comment,
  tree,
  currentUser,
}: {
  comment: Comment;
  tree: UseCommentTreeReturn;
  currentUser: CommentUser;
}) {
  const {
    isAuthor,
    edit,
    reply,
    reaction,
    showReplies,
    toggleReplies,
    deleteComment,
  } = useComment(comment, {
    onEdit: async (id, content) => tree.editComment(id, content),
    onReply: async (id, content) => tree.addReply(id, content),
    onReaction: async (id, reactionId) => tree.toggleReaction(id, reactionId),
    onDelete: async (id) => tree.deleteComment(id),
  });

  return (
    
      {/* Header */}
      
        **{comment.author.name}** ·{' '}
        {formatRelativeTime(comment.createdAt)}
      

      {/* Content / Edit mode */}
      {edit.isEditing ? (
        
           edit.setEditContent(e.target.value)}
            rows={3}
            style={{ width: '100%' }}
          />
          Save
          Cancel
        
      ) : (
        
{comment.content}

      )}

      {/* Action bar */}
      
         reaction.toggle('like')}>👍
        
          Reply
        
        {isAuthor && (
          <>
             edit.startEditing(comment.content)}>Edit
            Delete
          
        )}
      

      {/* Reply box */}
      {reply.isReplying && (
        
           reply.setReplyContent(e.target.value)}
            placeholder="Write a reply..."
            rows={2}
            style={{ width: '100%' }}
          />
          Submit
          Cancel
        
      )}

      {/* Nested replies */}
      {comment.replies?.length ? (
        <>
          
            {showReplies ? 'Hide' : 'Show'} {comment.replies.length}{' '}
            {comment.replies.length === 1 ? 'reply' : 'replies'}
          

          {showReplies &&
            comment.replies.map((r) => (
              
            ))}
        
      ) : null}
    
  );
}

/* --------------------------------------------------------------
   4️⃣ Comment list + new‑comment input + sorting
   -------------------------------------------------------------- */
function CommentList({
  tree,
  currentUser,
}: {
  tree: UseCommentTreeReturn;
  currentUser: CommentUser;
}) {
  const { sortedComments, sortOrder, setSortOrder } = useSortedComments(
    tree.comments,
    'newest',
  );
  const [text, setText] = useState('');

  return (
    
      {/* ---- Sort controls ---- */}
      
        {(['newest', 'oldest', 'popular'] as const).map((order) => (
           setSortOrder(order)}
            disabled={sortOrder === order}
            style={{
              fontWeight: sortOrder === order ? 'bold' : 'normal',
              marginRight: '0.5rem',
            }}
          >
            {order}
          
        ))}
      

      {/* ---- New comment input ---- */}
      
         setText(e.target.value)}
          placeholder="Leave a comment..."
          rows={3}
          style={{ width: '100%' }}
        />
         {
            if (text.trim()) {
              tree.addComment(text.trim());
              setText('');
            }
          }}
          disabled={!text.trim()}
        >
          Comment
        
      

      {/* ---- Render the comment thread ---- */}
      {sortedComments.map((comment) => (
        
      ))}
    
  );
}

What You Get

  • Fully functional comment thread (nestable, sortable, editable, deletable, reactable).
  • Zero CSS – you decide every pixel.
  • Zero opinionated markup – the hooks are completely decoupled from rendering.
  • Type‑safe API (all types are exported from the package).

TL;DR

  1. Install the package.
  2. Initialise a useCommentTree.
  3. Use useComment for each comment UI.
  4. Optionally add useSortedComments and formatRelativeTime.
  5. Build whatever UI you want on top of those hooks.

That’s literally all you need to have a headless, extensible comment system in a React app. 🎉

iy​a.dev – Platform‑Inspired Styles, Zero Lock‑In

iya.dev demonstrates 15+ platform‑inspired styles (Reddit, GitHub, Discord, Twitter, YouTube, …) that are all powered by the same hook calls underneath.

  • No design‑system lock‑in.
  • Works with Tailwind, CSS Modules, styled‑components, or plain CSS – the library stays completely out of your way.

Async Callbacks with useComment

Each callback you pass to useComment is async, making it the perfect place to fire off API calls before updating local state.

const { /* … */ } = useComment(comment, {
  onEdit: async (id, content) => {
    await fetch(`/api/comments/${id}`, {
      method: 'PATCH',
      body: JSON.stringify({ content }),
    });
    tree.editComment(id, content);
  },
  // … other handlers
});

Your data layer, your rules. The library does not care where your data lives or how you fetch it.

Features

FeatureDetails
Zero UINo styles or markup imposed
Full comment CRUDAdd, edit, delete, reply
ReactionsToggle any custom reaction
SortingNewest, oldest, most popular
TypeScriptFully typed out of the box
Backend agnosticWire up any API in async callbacks
npm install @hasthiya_/headless-comments-react

Check out the live showcase and the full documentation to see everything it can do.

0 views
Back to Blog

Related posts

Read more »

Improving Accessibility - Tooltip

!Cover image for Improving Accessibility - Tooltiphttps://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev...