I have a post page, where the user fetches data about a post with comments.
The user can create/delete comments and post.
However when the user deletes a comment I get his error:
Warning: Each child in a list should have a unique "key" prop.
Check the render method of `PostDetail`. See https://reactjs.org/link/warning-keys for more information.
PostDetail@webpack-internal:///./pages/post/[id].tsx:21:88
div
App@webpack-internal:///./pages/_app.tsx:31:38
PathnameContextProviderAdapter@webpack-internal:///./node_modules/next/dist/shared/lib/router/adapters.js:62:34
ErrorBoundary@webpack-internal:///./node_modules/next/dist/compiled/@next/react-dev-overlay/dist/client.js:301:63
ReactDevOverlay@webpack-internal:///./node_modules/next/dist/compiled/@next/react-dev-overlay/dist/client.js:850:908
Container@webpack-internal:///./node_modules/next/dist/client/index.js:61:1
AppContainer@webpack-internal:///./node_modules/next/dist/client/index.js:171:25
Root@webpack-internal:///./node_modules/next/dist/client/in
dex.js:346:37
I do not understand what the problem is, Every Comment has its on key prop when it gets "loaded".
This is my PostDetail.tsx page:
import React, { useCallback, useEffect, useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import styles from '../../styles/Post.module.css';
import formStyle from '../../styles/HomePage.module.css';
import { useRouter } from 'next/router';
import Post from '@/components/Post';
import { Comment } from '@/components/Comment';
interface CommentData{
'_id': string,
'creator': {
'username': string,
'_id': string
},
'comment': string,
'isOwner': boolean,
'createdAt': string,
}
interface PostData {
'post_creator': {
'username': string,
'_id': string
},
'_id': string,
'post_content': string,
'createdAt': string,
'comments': {
'_id': string,
'username': string
}
'likes': [],
'replies': []
}
const PostDetail = () => {
const [newComment, setNewComment] = useState<any>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [postData, setPostData] = useState<PostData>();
const [comments, setComments] = useState<any>([]);
const userData: any = localStorage.getItem('userData');
const parsedData = JSON.parse(userData)
const userID = parsedData.user_id
const router = useRouter();
const changeFormHeight = () => {
let textArea = (document.getElementById("NewPostForm") as HTMLTextAreaElement);
const numOfRows = textArea.value.split('\n').length;
if(textArea.value == ""){
textArea.style.height = "80px";
}
switch (true) {
case (numOfRows >= 7):
textArea.style.height = "200px";
break;
case (numOfRows >= 3):
textArea.style.height = "150px";
break;
default:
textArea.style.height = "80px";
break;
}
setNewComment(textArea.value)
}
const onSubmit = async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
event.preventDefault();
createNewComment();
}
const createNewComment = async () => {
console.log(comments)
const payload = {
'token': localStorage.getItem('token'),
'newComment': encodeURIComponent(newComment)
}
const body = JSON.stringify(payload);
try {
const response = await fetch(`http://localhost:8888/api/post/${postData?._id}/newComment`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": 'Bearer ' + payload.token
},
body: body,
})
const newCommentData: CommentData = await response.json();
// set the isOwner value based on the current user's ID
const userData: any = localStorage.getItem('userData');
const parsedData = JSON.parse(userData);
const userID = parsedData.user_id;
newCommentData.isOwner = userID === newCommentData.creator._id;
setComments((prevComments: CommentData[]) => [...prevComments, newCommentData]);
let textArea = (document.getElementById("NewPostForm") as HTMLTextAreaElement);
textArea.value = "";
textArea.style.height = "80px";
setNewComment("");
console.log(comments)
} catch (error) {
console.log(error);
}
}
const fetchPostData = async (postID: any) => {
setIsLoading(true);
const payload = {
'token': localStorage.getItem('token'),
}
const response = await fetch(`http://localhost:8888/api/post/post/${postID}`, {
method: "GET",
headers: {
"Authorization": 'Bearer ' + payload.token
}
})
const data: PostData = await response.json();
setIsLoading(false);
setPostData(data);
setComments(data.comments)
console.log(data.comments)
}
const getPostID = useCallback(() => {
return router.query.id
}, [router.query.id])
useEffect(() => {
const postID = getPostID();
if (postID) {
fetchPostData(postID);
}
}, [getPostID]);
const renderComment = (_id:string, creator:any, comment:string, createdAt:string, postID:any) => {
const isOwner = userID === creator._id
return(
<>
<Comment
key={_id}
_id={_id}
creator={creator}
comment={comment}
isOwner={isOwner}
createdAt={createdAt}
postID={postID}
/>
</>
)
}
return (
<>
{postData && (
<Post
key={postData._id}
_id={postData._id}
post_creator={postData.post_creator}
post_content={postData.post_content}
likes={postData.likes}
replies={postData.replies}
createdAt={postData.createdAt}
type='DETAIL'
isOwner={userID === postData.post_creator._id}
/>
)}
<div className={`${formStyle.NewPostContainer} ${formStyle.BorderTop}`}>
<h2 className={formStyle.NewFormTitle}>Create new comment: </h2>
<form className={formStyle.Form} onSubmit={onSubmit}>
<textarea id='NewPostForm' onChange={changeFormHeight}/>
<div className={`${formStyle.FormToolBar} ${formStyle.Flex_reverse}`}>
<button className={formStyle.PostButton}>Post new comment</button>
</div>
</form>
</div>
{ !isLoading && comments.map((comment: CommentData) => (
renderComment(comment._id, comment.creator, comment.comment, comment.createdAt, postData?._id)
))}
</>
)
}
export default PostDetail
And this is my Comment.tsx:
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import React from 'react'
import styles from '../styles/Comment.module.css'
interface CommentProps{
'_id': string,
'creator': {
'username': string,
'_id': string
},
'comment': string,
'isOwner': boolean,
'createdAt': string,
'postID': string
}
export const Comment = ({ _id, creator, comment, isOwner, createdAt, postID } : CommentProps) => {
const likeButtonHeight = !isOwner ? styles.fullHeight : '';
const deletButtonHeight = isOwner ? styles.fullHeight : '';
const router = useRouter();
function timeDifference(current: any, previous: any): string {
const intervals = {
year: 31536000,
month: 2592000,
day: 86400,
hour: 3600,
minute: 60,
second: 1,
};
const secondsElapsed = Math.floor((current - previous) / 1000);
for (const [key, value] of Object.entries(intervals)) {
const count = Math.floor(secondsElapsed / value);
if (count >= 1) {
return count === 1 ? `1 ${key} ago` : `${count} ${key}s ago`;
}
}
return "Just now";
}
let timestamp = timeDifference(new Date(), new Date(createdAt));
const deleteComment = async() => {
const payload = {
'token': localStorage.getItem('token'),
}
await fetch(`http://localhost:8888/api/post/${postID}/comment/${_id}`, {
method: "DELETE",
headers: {
"Authorization": 'Bearer ' + payload.token
}
})
router.reload()
}
return (
<div key={_id} className={styles.Comment}>
<h1>{_id}</h1>
<Link href={`/profile/${creator._id}`} className={styles.CommentLeftBody}>
<Image className={styles.UserImage} src='/images/user_icon.png' width="512" height="512" alt='User profile image'/>
<div>
<h2 className={styles.UserName}>{creator.username}</h2>
</div>
</Link>
<div className={styles.CommentBody}>
<p>
{decodeURIComponent(comment)}
</p>
<h3 className={styles.TimeStamp}>{timestamp}</h3>
</div>
<div className={styles.CommentToolbar}>
{ !isOwner &&
<button className={`${styles.LikeButton} ${likeButtonHeight}`}>
<i className="fa-solid fa-heart"></i>
</button>
}
{ isOwner &&
<button onClick={deleteComment} className={`${styles.DeleteButton} ${deletButtonHeight}`}>
<i className="fa-solid fa-trash-can"></i>
</button>
}
</div>
</div>
)
}