Adding a comment section to a React app sounds simple. But then you try it, and 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. You end up spending more time fighting the library than building your product.
So what if you could get all the hard logic handled for you, and still keep complete control over how everything looks and feels? That is 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 handle all the state and logic for a comment thread, including adding, replying, editing, deleting, reacting, and sorting, without rendering a single <div>. You bring the UI. The library brings the brains.
That separation is what makes this powerful. You can make your comment section look like Reddit, Discord, GitHub, or anything else you can design, all using the exact same underlying hooks.
Installation
npm install @hasthiya_/headless-comments-react
Core Concepts
The library is built around two main hooks, both imported from @hasthiya_/headless-comments-react/headless.
useCommentTree manages the entire comment thread. It holds the list of comments and gives you methods for adding, editing, deleting, and reacting.
useComment manages a single comment. It gives you everything you need for that one comment: reply state, edit state, reaction toggles, and a boolean to check if the current user is the author.
On top of those, useSortedComments handles sorting by newest, oldest, or most popular, and the formatRelativeTime utility turns timestamps into readable strings like "5 minutes ago".
Quick Setup
Here is everything you need to get a fully working comment thread off the ground.
1. Define Your 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. Set Up the Comment Tree
import { useCommentTree } from '@hasthiya_/headless-comments-react/headless';
function CommentSection() {
const tree = useCommentTree({
initialComments: [], // load your existing comments here
});
return <CommentList tree={tree} currentUser={currentUser} />;
}
3. Render Your Comments
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 (
<div>
<p><strong>{comment.author.name}</strong> · {formatRelativeTime(comment.createdAt)}</p>
{edit.isEditing ? (
<div>
<textarea value={edit.editContent} onChange={e => edit.setEditContent(e.target.value)} />
<button onClick={edit.submitEdit}>Save</button>
<button onClick={edit.cancelEdit}>Cancel</button>
</div>
) : (
<p>{comment.content}</p>
)}
<div>
<button onClick={() => reaction.toggle('like')}>👍</button>
<button onClick={reply.isReplying ? reply.cancelReply : reply.openReply}>Reply</button>
{isAuthor && (
<>
<button onClick={() => edit.startEditing(comment.content)}>Edit</button>
<button onClick={deleteComment}>Delete</button>
</>
)}
</div>
{reply.isReplying && (
<div>
<textarea
value={reply.replyContent}
onChange={e => reply.setReplyContent(e.target.value)}
placeholder="Write a reply..."
/>
<button onClick={reply.submitReply}>Submit</button>
<button onClick={reply.cancelReply}>Cancel</button>
</div>
)}
{comment.replies && comment.replies.length > 0 && (
<>
<button onClick={toggleReplies}>
{showReplies ? 'Hide' : 'Show'} {comment.replies.length} replies
</button>
{showReplies && comment.replies.map(r => (
<CommentItem key={r.id} comment={r} tree={tree} currentUser={currentUser} />
))}
</>
)}
</div>
);
}
4. Add a Top-Level Composer and Sorting
function CommentList({
tree,
currentUser,
}: {
tree: UseCommentTreeReturn;
currentUser: CommentUser;
}) {
const { sortedComments, sortOrder, setSortOrder } = useSortedComments(tree.comments, 'newest');
const [text, setText] = useState('');
return (
<div>
{/* Sort controls */}
{(['newest', 'oldest', 'popular'] as const).map(order => (
<button key={order} onClick={() => setSortOrder(order)}>
{order}
</button>
))}
{/* New comment input */}
<textarea value={text} onChange={e => setText(e.target.value)} placeholder="Leave a comment..." />
<button
onClick={() => { if (text.trim()) { tree.addComment(text.trim()); setText(''); } }}
disabled={!text.trim()}
>
Comment
</button>
{/* Comment list */}
{sortedComments.map(comment => (
<CommentItem key={comment.id} comment={comment} tree={tree} currentUser={currentUser} />
))}
</div>
);
}
And that is genuinely all you need. You now have a fully functional, sortable, nestable comment thread with replies, edits, deletes, and reactions, with zero imposed styles and zero opinionated markup.
The Real Power: Bring Your Own UI
Because the hooks are completely decoupled from rendering, you can plug the exact same logic into any UI you want. The showcase at headless.hasthiya.dev proves this with 15+ platform-inspired styles including Reddit, GitHub, Discord, Twitter, and YouTube, all powered by identical hook calls underneath.
This means you are not locked into any design system. Whether you reach for Tailwind, CSS Modules, styled-components, or plain CSS, the library stays completely out of your way.
Connecting to a Real Backend
Each callback you pass to useComment is async, which makes it the perfect place to fire off API calls before updating local state. Here is what that looks like in practice:
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.
Summary
| 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 |
If you have ever wanted a comment system that fits into your app instead of the other way around, this is the library for you.
npm install @hasthiya_/headless-comments-react
Check out the live showcase and the full documentation to see everything it can do.


Top comments (0)