Hello there! In this article, I’ll guide you through building a nested comment system in React.js in no time. Many developers, especially beginners, often find this logic difficult to implement. But trust me, it's simpler than it looks! So, without further ado, let’s dive in.
Key Features
- Sign in/out
- Commenting
- Replying to comments
- Editing/Deleting own comments and replies
Requirements:
Before we start, here are a few things you’ll need to set up:
1. User Sign-Up: In my example, I’ve implemented a basic sign-up system to establish user identity. You can choose a different strategy if you prefer. The key point is to have a way of identifying users.
2. Session Storage: I’ve used session storage to save comments and user information. However, when deploying this to production, you’ll need to implement a more robust method to manage user data and persistence.
Sign In / Sign Out Logic
In this implementation, I have used session storage to manage user sign-ins and sign-outs. But feel free to customize this according to your needs. The main goal here is to manage user sessions effectively to enable commenting and replying.
const signin = () => {
if (!session) {
sessionStorage.setItem('user', user)
alert('Signin succussfully!')
setUser('')
window.location.reload();
}
}
const signOut = () => {
if (session) {
sessionStorage.removeItem('user')
alert('SignOut succussfully!')
window.location.reload();
}
}
Understanding the Nested Structure
The core logic of a nested comment system revolves around **looping**. Since this is a nested structure, the challenge lies in managing replies under comments. Each reply is essentially a comment itself but tied to another comment (its parent). Therefore, we need to check for child comments and pass the data down as props to the same component to handle the nested replies.
Reply Feature
The reply functionality also relies on looping. You’ll need to check the ID of the comment that the user is replying to. If a user replies to a reply, the second loop comes into play to handle that. The logic ensures that all replies are nested under the correct parent comment.
const reply = (id, msg) => {
const insertComment = (comments) => {
for (let i = 0; i < comments.length; i++) {
const comment = comments[i];
if (comment.id === id) {
// pushing children to comment;
if (comment.Children) {
comment.Children.unshift({
id: Math.random(),
user: sessionStorage.getItem('user'),
message: msg,
})
} else {
const newChildren = [{
id: Math.random(),
user: sessionStorage.getItem('user'),
message: msg,
}]
comment.Children = newChildren;
}
}
}
for (let i = 0; i < comments.length; i++) {
let comment = comments[i];
if (comment.Children) {
insertComment(comment.Children)
}
}
}
App.js
import { useEffect, useState } from 'react';
import Mycomments from './mycomments';
import { removeEmptyObjects } from 'remove-empty-object'
export default function App() {
const [message, setMessage] = useState(JSON.parse(sessionStorage.getItem('messages')) || []);
const [text, setText] = useState('');
const [user, setUser] = useState('');
const [session, setSession] = useState(sessionStorage.getItem('user'));
const [replyBox, setReplyBox] = useState(false);
const [refresh, setRefresh] = useState(Math.random())
const signin = () => {
if (!session) {
sessionStorage.setItem('user', user)
alert('Signin succussfully!')
setUser('')
window.location.reload();
}
}
const signOut = () => {
if (session) {
sessionStorage.removeItem('user')
alert('SignOut succussfully!')
window.location.reload();
}
}
const handleChange = (e) => {
e.preventDefault();
if (!session) {
alert('You are not logged in!')
} else {
const newMsg = {
id: Math.random(),
user: sessionStorage.getItem('user'),
message: text,
}
setMessage(message => [...message, newMsg])
setText('')
}
}
useEffect(() => {
const filterMsgs = [...message]
const filerdata = filterMsgs.filter(item => removeEmptyObjects(item))
sessionStorage.setItem('messages', JSON.stringify(filerdata))
}, [message, refresh])
const del = (id) => {
const nList = message.filter(item => item.id !== id)
setMessage(nList)
}
const reply = (id, msg) => {
const insertComment = (comments) => {
for (let i = 0; i < comments.length; i++) {
const comment = comments[i];
if (comment.id === id) {
// pushing children to comment;
if (comment.Children) {
comment.Children.unshift({
id: Math.random(),
user: sessionStorage.getItem('user'),
message: msg,
})
} else {
const newChildren = [{
id: Math.random(),
user: sessionStorage.getItem('user'),
message: msg,
}]
comment.Children = newChildren;
}
}
}
for (let i = 0; i < comments.length; i++) {
let comment = comments[i];
if (comment.Children) {
insertComment(comment.Children)
}
}
}
const commentswithnewreply = [...message];
insertComment(commentswithnewreply)
setMessage(commentswithnewreply)
setReplyBox(false);
}
const remove = (id) => {
const updateComments = (comments) => {
if (comments) {
for (let i = 0; i < comments.length; i++) {
let comment = comments[i]
if (comment.id === id && comment.user === session) {
for (let key in comment) {
delete comment[key]
}
}
}
for (let i = 0; i < comments.length; i++) {
let comment = comments[i]
updateComments(comment.Children)
}
}
}
const updatedComments = [...message];
updateComments(updatedComments)
setMessage(updatedComments)
setRefresh(Math.random())
}
return (
<>
<div className='flex items-center w-[500px] mx-auto border p-2 mt-16'>
<input
value={user}
onChange={(e) => setUser(e.target.value)}
className='w-full outline-none'
type='email'
placeholder='Your Name' />
{session ?
<button
onClick={signOut}
className='bg-red-600 px-4 rounded text-white p-2'>
SignOut
</button> :
<button
onClick={signin}
className='bg-blue-400 px-4 rounded text-white p-2'>
SignIn
</button>
}
</div>
{sessionStorage.getItem('user') &&
<span className='flex w-[500px] mx-auto text-xs'>
You: {sessionStorage.getItem('user')}
</span>
}
<form
onSubmit={handleChange}
className='w-[500px] flex flex-col mx-auto'
>
<input
value={text}
onChange={(e) => setText(e.target.value)}
className='border w-[500px] flex mt-4 p-4 outline-none'
placeholder='Your message'
/>
<button className='p-3 bg-blue-400 text-white'>SUBMIT</button>
</form>
{message && message?.map(val => (
<div key={Math.random()} className='w-max mx-auto flex my-4'>
<Mycomments comment={val} reply={reply} del={del} replyBox={replyBox} remove={remove} />
</div>
))}
<br></br>
<br></br>
</>
);
}
mycomments.js
import { useState } from "react";
import { MdOutlineReply } from "react-icons/md";
import { MdRemoveCircleOutline } from "react-icons/md";
export default function Mycomments(props) {
const [replyText, setReplyText] = useState('');
const [replyBx, setReplyBx] = useState(false);
const replyBox = (data) => {
setReplyBx(data)
}
return (
<>
{props.comment?.user && props.comment?.message &&
<li className='flex flex-col mx-auto w-[500px]'>
<div className='flex flex-col'>
<div className="flex flex-col w-full mb-4">
<div className="flex flex-col min-w-[300px] rounded overflow-hidden bg-white z-10">
<span className='text-xs text-white w-max px-4 bg-[#4a6dde]'>
{props.comment.user}
</span>
<span className='border-b p-2 px-2 w-[-webkit-fill-available] w-full border-l'>
{props.comment.message}
</span>
</div>
<div className="flex gap-4 p-2">
{props.comment.user === sessionStorage.getItem('user') &&
<button
onClick={() => props.remove(props.comment.id)}
className='text-red-400 text-xs'>
<MdRemoveCircleOutline size={18} />
</button>
}
{sessionStorage.getItem('user') &&
<button
onClick={() => setReplyBx(true)}
className='text-gray-400 text-xs'>
<MdOutlineReply size={18} />
</button>
}
</div>
</div>
{props.comment.Children &&
<span className="text-xs text-green-500 font-semibold p-1 px-4 rounded-full ml-8 mb-4 bg-black w-max">
Replies
</span>
}
{/* reply box */}
{replyBx &&
<div className="flex text-white w-max pl-10 pb-2 mb-4">
<input type='text'
className='border outline-none p-2 w-full text-black'
placeholder='reply'
onChange={(e) => setReplyText(e.target.value)} />
<button className='px-4 bg-blue-500'
onClick={() => props.reply(props.comment.id, replyText)}>
reply
</button>
</div>
}
<ul className="relative w-full pl-10 border-gray-400 item-center" >
{props.comment.Children?.map(childComment => (
<Mycomments
key={Math.random()}
comment={childComment}
reply={props.reply}
del={props.del}
replyBox={replyBox}
remove={props.remove}
/>
))}
</ul>
</div>
</li>
}
</>
)
}
By following these steps, you can efficiently build a nested comment system in React.js. It’s a straightforward process once you grasp the looping logic and manage user identities properly.