Kevin H
Kevin H

Reputation: 113

React component not updating after prop is updated using setState

I am currently trying to create an interactive comment section that allows users to comment, reply, and like/dislike the comments of others. I am currently stuck on the likes/upvote section, as I am unable to update the value of likes in my component.

Here is my Comment component (which represents a single comment in the comment section):

import { useState } from "react";

const Comment = ({
  upvotes,
  image,
  username,
  isUser,
  replies,
  createdAt,
  id,
  upvoteMethod,
}) => {
  const [score, setScore] = useState(upvotes);
  return (
    <div className="comment-container">
      <div className="comment">
        <div className="upvotes-section">
          <div className="upvotes">
            <img
              id="upvote"
              src="/images/icon-plus.svg"
              onClick={() => {
                upvoteMethod(id, "upvote");
              }}
            ></img>
            <h3>{score}</h3>
            <img
              id="downvote"
              src="/images/icon-minus.svg"
              onClick={upvoteMethod}
            ></img>
          </div>
        </div>
        <div className="comment-side">
          <div className="comment-header">
            <div className="profile">
              <img src={image}></img>
              <h5>{username}</h5>
              <h6 className="created-at">{createdAt}</h6>
            </div>
            <div className="options">
              <img src={isUser ? "images/icon-delete.svg" : ""}></img>
              <img src="/images/icon-reply.svg"></img>
            </div>
          </div>
        </div>
      </div>

      {replies.map((r) => {
        return (
          <div className="replies-section">
            <img src={isUser ? "images/icon-delete.svg" : ""}></img>
          </div>
        );
      })}
    </div>
  );
};

export default Comment;

And here is the App.jsx file, where I pass down the update function for my upvote counter:

import Comment from "./components/Comment";
import "./App.css";
import CommentArea from "./components/CommentArea";
import TextField from "./components/TextField";
import { useState } from "react";

function App() {
  let displayedComments = [
    {
      id: 1,
      content:
        "Impressive! Though it seems the drag feature could be improved. But overall it looks incredible. You've nailed the design and the responsiveness at various breakpoints works really well.",
      createdAt: "1 month ago",
      score: 12,
      user: {
        image: {
          png: "./images/avatars/image-amyrobson.png",
          webp: "./images/avatars/image-amyrobson.webp",
        },
        username: "amyrobson",
      },
      replies: [],
    },
    {
      id: 2,
      content:
        "Woah, your project looks awesome! How long have you been coding for? I'm still new, but think I want to dive into React as well soon. Perhaps you can give me an insight on where I can learn React? Thanks!",
      createdAt: "2 weeks ago",
      score: 5,
      user: {
        image: {
          png: "./images/avatars/image-maxblagun.png",
          webp: "./images/avatars/image-maxblagun.webp",
        },
        username: "maxblagun",
      },
      replies: [
        {
          id: 3,
          content:
            "If you're still new, I'd recommend focusing on the fundamentals of HTML, CSS, and JS before considering React. It's very tempting to jump ahead but lay a solid foundation first.",
          createdAt: "1 week ago",
          score: 4,
          replyingTo: "maxblagun",
          user: {
            image: {
              png: "./images/avatars/image-ramsesmiron.png",
              webp: "./images/avatars/image-ramsesmiron.webp",
            },
            username: "ramsesmiron",
          },
        },
        {
          id: 4,
          content:
            "I couldn't agree more with this. Everything moves so fast and it always seems like everyone knows the newest library/framework. But the fundamentals are what stay constant.",
          createdAt: "2 days ago",
          score: 2,
          replyingTo: "ramsesmiron",
          user: {
            image: {
              png: "./images/avatars/image-juliusomo.png",
              webp: "./images/avatars/image-juliusomo.webp",
            },
            username: "juliusomo",
          },
        },
      ],
    },
  ];

  const [comments, setComment] = useState(displayedComments);
  const updateComments = (event) => {
    setComment(comments);
  };

  const upvotePost = (id, action) => {
    for (let i = 0; i < comments.length; i++) {
      if (comments[i].id == id) {
        action === "upvote" ? comments[i].score++ : comments[i]--;
        setComment(comments);
        console.log(action);
      }
    }
  };

  return (
    <div className="App">
      <CommentArea upvoteMethod={upvotePost} comments={comments}></CommentArea>
      <TextField></TextField>
    </div>
  );
}

export default App;

When I try to console log the comments array after using setState(), it shows that the value has updated after clicking the buttons, but it does not show on the UI.

for reference here is a screenshot of me debugging in the console (upvotes is comments.score, and I clicked on the first comment/the one with 12 upvotes)

enter image description here

Can anyone help me understand what is going on and how to fix it? help would be appreciated.

Upvotes: 1

Views: 1443

Answers (1)

Nick Parsons
Nick Parsons

Reputation: 50974

State updates in reactjs should be immutable, you can think of this as meaning that you shouldn't update/mutate your current state in any way, but rather, create a new state value and apply your changes to that. You're currently mutating your state and then setting your state back to the mutated state value:

action==="upvote" ? comments[i].score++ : comments[i]--
setComment(comments)

From React's perspective, the new comments array that you're setting as your state is the same as your old comments state (ie: if you were to compare the old state and the new state with ===, you would get back true), so React doesn't rerender as it can't see the state change. Instead, you should be creating a new comments array (below this is created with .map()), and then creating new inner comment objects when you want to update the score value:

const upvotePost=(id, action)=> {
  const mult = action === "upvote" ? 1 : -1;
  setComment(
    comments => comments.map(comment => comment.id === id
      ? {...comment, score: comment.score + (1*mult)}
      : comment
    )
  ); 
}

Notice that above we never update the comments state directly, but instead return a new array, with a new object for the ones we want to update. The new object is created by using {...comment, score: comment.score + (1*mult)}, where the ...comment adds all of the (own enumerable) keys from the current object (see the spread syntax ...), and then sets the score key on the newly created object to an updated value. The arrow function in the setComments() is being used to obtain the most up to date version of comments before we update it - see functional updates.

Another issue is that you don't need your score state in your Comment component. The useState(upvotes) sets the score value to the value of upvotes on the intial render of your component, but when the upvotes prop changes, your score state won't change with it. As you're not using setScore, you can remove this state and instead use upvotes directly.

Upvotes: 2

Related Questions