Home > Software design >  Is there a way to update the state from child component in nextjs?
Is there a way to update the state from child component in nextjs?

Time:10-15

In my Nextjs app I am using getServerSideProps to pull in data from my database for the [url].tsx route (the url corresponds to a specific project in the database). From there I can populate the page's content with things like {project.name} etc etc. Underneath the projects information I have 2 sections that allow you to add a comment and view all the comments for that project.

Add a comment section:

  • Form with submit (child component)

View Comments section: (comments are stored in table with rating, name, message, and comment id which references the project id)

  • Comments stats (child component)
  • Displays total # of comments
  • Displays the Avg rating of the current project

In [url].tsx, I thought it would be nice If I could update the avg rating and add the newly created comment to the section without a refresh.

I was reading about Reacts useState and set up:

const [comments, setComments] = useState(project.comments)

From there though, I got a bit confused since I am needed to pass setComments to my CommentStats component and to my CommentGrid component (Comment grid sets the layout of the comments which are pulled in as a single component as well) once my CommentForm component submits the data to my api route (to be added to the database).

I guess since I split everything up has made this even more confusing to me.

CommentStats.tsx

type CommentStatsProps = {
    comments: any;
}


const CommentStats = ({ comments }: CommentStatsProps) => {
    const totalComments = comments.length;
    const totalStars = Comments.reduce((acc: any, curr: { rating: any; }) => acc   curr.rating, 0);
    const averageStars = comments / totalComments;
    
    return (
        <div className="max-w-4xl mx-auto flex justify-between items-center p-8 my-12 bg-white rounded-lg shadow-lg">
            <div className="flex items-center p-4 bg-violet-500 rounded-lg space-x-4">
                <h1 className="text-4xl font-bold text-white">{averageStars.toFixed(1)}</h1>
                <FaStar className="h-5 text-3xl text-yellow-300" />
            </div>
            <div className="flex items-center p-4 bg-slate-500 rounded-lg space-x-4">
                <p className="text-2xl text-white">
                    <span className="text-4xl font-bold">{totalComments}</span> {totalComments === 1 ? "Comment" : "Comments"}
                </p>
            </div>
        </div>
    );
}

export default CommentStats

CommentGrid.tsx


import CommentCard from "@/components/Comments/CommentCard";

/* eslint-disable @typescript-eslint/no-explicit-any */
type CommentsProps = {
    comments: any;
}

type CommentProps = {
    id: string;
    name: string;
    comment: string;
    rating: number;
    createdAt: string;
}

const CommentGrid = ({ comments }: CommentsProps) => {

    const totalComments = comments.length;

    const left = comments.slice(0, Math.ceil(totalComments / 2));
    const right = comments.slice(Math.ceil(totalComments / 2), totalComments);

    const list = {
        visible: { 
            opacity: 1,
            y: 0,
            transition: {
                when: "beforeChildren",
                staggerChildren: 0.5
            }
         },
        hidden: { opacity: 0, y: -100 }
    }

    const item = {
        visible: { opacity: 1, y: 0 },
        hidden: { opacity: 0, y: -100 }
    }

    // Check if there are any comments, if not return a message
    if (totalComments === 0) {
        return (
            <div className="tracking-wider text-center">
                <h1 className="text-4xl pb-4">No Comments Yet</h1>
                <p className="text-2xl">Be the first to leave a comment!</p>
            </div>
        )
    }

    return (
        
        <motion.div
            // initial={{ opacity: 0 }}
            // animate={{ opacity: 1 }}
            // whileInView={{ opacity: 1 }}
            // transition={{ duration: 0.5 }}
            // viewport={{ once: true}}
            className="grid md:grid-cols-2 gap-8">
        
            {/* Left */}
            <motion.ul
                whileInView={{ opacity: 1 }}
                initial="hidden"
                animate="visible"
                variants={list}
                className="space-y-8">
                {left.map((comment: CommentProps) => (
                    <motion.li 
                        variants={item}
                        key={comment.id}
                    >

                        <CommentCard stars={comment.rating} author={comment.name} comment={comment.comment} date={comment.createdAt} />
                    </motion.li>
                ))}
            </motion.ul>

        </motion.div>

    )

}

export default CommentGrid

I was not trying to make this too complicated, but I thought the added "real time" update would be nice while the api route is actually handling the creation in the background. Is there an "easy" way to go about this?

CodePudding user response:

When you want sibling components to communicate, you need to do it via their parent.

You should create a function called for example handleNewComment in [url].tsx, and you should pass this function to CommentForm. CommentForm should, after submitting a new comment to the database, it should call handleNewComment and pass the new comment to this function.

(this is in CommentForm) I assume you have a function similar to this:

const handleSave = (newComment) => {
  // here you are sending data to your database
  handleNewComment(newComment); // this you should add so you can communicate back to the parent that a new comment has been added
}

(this is in [url].tsx) In handleNewComment you should do something like this:

const handleNewComment = (newComment) => {
  setComments([...comments], newComment);
}

=> meaning you add the new comment to comments (which are a part of state)

CommentStats and CommentForm get comments from [url].tsx], so they are now displaying updated comments (those from database new ones)

CodePudding user response:

You could pass a function that updates the parent component to the child. You could define such a function in the parent component:

const addComment = (comment: string) => {
  setComments(prev => [...prev, comment]);
}

This is assuming that project.comments is an array and is spreadable. You could then pass this function as a prop to the child component that can add comments:

interface ChildProps {
  addComment: (comment: string) => void;
}

const Child = ({addComment}: ChildProps) => {
  ...
  // just call "addComment(comment)" when you need to, it will update the 
  // the parent component "comment" variable
}

There is an issue though, if the call to your database fails, you will have to remove the comment, which is a bit tricky. You could use GUIDs, or combinations of username timestamp to identify which comment exactly needs to be deleted in case of API failure.

Another thing to keep in mind is the fact that react components and all their children are re-rendered if the state changes - if the parent component contains children that don't depend on the variables being updated, its unnecessary re-renders (but if all the children do depend on the variable, then that is good because they will all be consistent and have a single source of truth). Dan Abramov has a great article about presentation and container components which has a brilliant explanation.

Personally, I think it is better to have the UI be reflective of the database state, only updating the count when you received an OK response about adding the comment to the database.

Alternitavely, you could do all the calculations on the back-end instead, send back a response with the "stats" for you to display.

I agree with how you split out the code, it seems very reasonable, but I'm not sure about have comments of type any. It's better to have a type for them imo :)

  • Related