-3

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>
    )
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
mitas1c
  • 301
  • 3
  • 13

1 Answers1

2

Key should be on the fragment:

<React.Fragment key={_id}>
  <Comment
    _id={_id}
    creator={creator}
    comment={comment}
    isOwner={isOwner}
    createdAt={createdAt}
    postID={postID}
  />
</React.Fragment>;

Or remove the fragment:

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}
    />
  );
};
Konrad
  • 21,590
  • 4
  • 28
  • 64