Build a Custom Comment Section
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)
| Hook | Responsibility |
|---|---|
useCommentTree | Manages the whole comment thread – list of comments + CRUD + reaction methods. |
useComment | Manages a single comment – reply state, edit state, reaction toggles, author check. |
useSortedComments | Provides 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
- Install the package.
- Initialise a
useCommentTree. - Use
useCommentfor each comment UI. - Optionally add
useSortedCommentsandformatRelativeTime. - 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. 🎉
iya.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
| Feature | Details |
|---|---|
| Zero UI | No styles or markup imposed |
| Full comment CRUD | Add, edit, delete, reply |
| Reactions | Toggle any custom reaction |
| Sorting | Newest, oldest, most popular |
| TypeScript | Fully typed out of the box |
| Backend agnostic | Wire 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.