3

I have this page which shows a single post and I have a like button. if the post is liked, when the user clicks the button, it changes its state to unlike button, but if the post is not liked, then the like is getting registered and the id is getting pushed on to the array, but the button state is not getting updated and I have to reload the page to see the page. Can someone tell me how to resolve this issue?

This is the code:

    const [liked, setLiked] = useState(false)

    const [data, setData] = useState([]);

    function likePosts(post, user) {
        post.likes.push({ id: user });
        setData(post);
        axiosInstance.post('api/posts/' + post.slug + '/like/');
        window.location.reload()
    }

    function unlikePosts(post, user) {
        console.log('unliked the post');
        data.likes = data.likes.filter(x => x.id !== user);
        setData(data);
        return (
            axiosInstance.delete('api/posts/' + post.slug + '/like/')
        )
    }

For the button:

{data.likes && data.likes.find(x => x.id === user) ?
        (<FavoriteRoundedIcon style={{ color: "red" }}
            onClick={() => {
                unlikePosts(data, user)
                setLiked(() => liked === false)
            }   
        }
        />)
        : (<FavoriteBorderRoundedIcon
               onClick={() => {
               likePosts(data, user)
               setLiked(() => liked === true)
               }
        }
        />)
    }

Thanks and please do ask if more details are needed.

11
  • You shouldn't mutate the state directly; use setData. Commented Oct 25, 2020 at 16:59
  • Hey, can you please explain a bit more? Commented Oct 25, 2020 at 17:00
  • I cannot really test your code, but you are assigning to data.likes which shouldn't be done. Also, what is data supposed to be? Your initial state is an array, but it seems like you are using it as an object. Commented Oct 25, 2020 at 17:01
  • 1
    Yes, so it should be an object (you are using useState([])). You can change it with something like setData(data => {...data, likes: data.likes.filter(...)}). May I ask, what is setLiked(() => liked === false) supposed to do? Why not setLiked(false)? Commented Oct 25, 2020 at 17:04
  • 1
    You are also mutating the state directly here: post.likes.push(...). I think the best way would be to use console logging or React DevTools to debug your code, as it's hard to help if I can't reproduce it. Commented Oct 25, 2020 at 17:08

1 Answer 1

2

As @iz_ pointed out in the comments, your main problem is that you are directly mutating state rather than calling a setState function.

I'm renaming data to post for clarity since you have said that this is an object representing the data for one post.

const [post, setPost] = useState(initialPost);

You don't need to use liked as a state because we can already access this information from the post data by seeing if our user is in the post.likes array or not. This allows us to have a "single source of truth" and we only need to make updates in one place.

const isLiked = post.likes.some((like) => like.id === user.id);

I'm confused about the likes array. It seems like an array of objects which are just {id: number}, in which case you should just have an array of ids of the users who liked the post. But maybe there are other properties in the object (like a username or timestamp).

When designing a component for something complex like a blog post, you want to break out little pieces that you can use in other places of your app. We can define a LikeButton that shows our heart. This is a "presentation" component that doesn't handle any logic. All it needs to know is whether the post isLiked and what to do onClick.

export const LikeButton = ({ isLiked, onClick }) => {
  const Icon = isLiked ? FavoriteRoundedIcon: FavoriteBorderRoundedIcon;
  return (
    <Icon
      style={{ color: isLiked ? "red" : "gray" }}
      onClick={onClick}
    />
  );
};

A lot of our logic regarding liking and unliking could potentially be broken out into some sort of usePostLike hook, but I haven't fully optimized this because I don't know what your API is doing and how we should respond to the response that we get.

When a user clicks the like button we want the changes to be reflected in the UI immediately, so we call setPost and add or remove the current user from the likes array. We have to set the state with a new object, so we copy all of the post properties that are not changing with the spread operator ...post and then override the likes property with an edited version. filter() and concat() are both safe array functions which return a new copy of the array.

We also need to call the API to post the changes. You are using the same url in both the "like" and "unlike" scenarios, so instead of calling axios.post and axios.delete, we can call the generalized function axios.request and pass the method name 'post' or 'delete' as an argument to the config object. [axios docs] We could probably combine our two setPost calls in a similar way and change likePost() and unlikePost() into one toggleLikePost() function. But for now, here's what I've got:

export const Post = ({ initialPost, user }) => {
  const [post, setPost] = useState(initialPost);

  const isLiked = post.likes.some((like) => like.id === user.id);

  function likePost() {
    console.log("liked the post");
    // immediately update local state to reflect changes
    setPost({
      ...post,
      likes: post.likes.concat({ id: user.id })
    });
    // push changes to API
    apiUpdateLike("post");
  }

  function unlikePost() {
    console.log("unliked the post");
    // immediately update local state to reflect changes
    setPost({
      ...post,
      likes: post.likes.filter((like) => like.id !== user.id)
    });
    // push changes to API
    apiUpdateLike("delete");
  }

  // generalize like and unlike actions by passing method name 'post' or 'delete'
  async function apiUpdateLike(method) {
    try {
      // send request to API
      await axiosInstance.request("api/posts/" + post.slug + "/like/", { method });
      // handle API response somehow, but not with window.location.reload()
    } catch (e) {
      console.log(e);
    }
  }

  function onClickLike() {
    if (isLiked) {
      unlikePost();
    } else {
      likePost();
    }
  }

  return (
    <div>
      <h2>{post.title}</h2>
      <div>{post.likes.length} Likes</div>
      <LikeButton onClick={onClickLike} isLiked={isLiked} />
    </div>
  );
};

CodeSandbox Link

Sign up to request clarification or add additional context in comments.

7 Comments

Thanks a lot for the answer! I will try this way :)
Hey @Linda, I tried this, but I am getting an error in this line, const isLiked = post.likes.some((like) => like.id === user.id); and this is the error TypeError: Cannot read property 'likes' of undefined. Can you please look into it? and as you said, the likes array contains the id and username of users.
This is my console.log(post) looks like id: 152 likes: [] post_date: "2020-10-25T09:21:14.531431+01:00" postimage_set: (2) [{…}, {…}] display_pic: "http://127.0.0.1:8000/media/pimage/1/profile_pic_C68UK9J.png" slug: "just-checking-multiple-upload" title: "Just checking multiple upload!" user: "testuser12" but when I try console.log(post.likes) it throws this error TypeError: Cannot read property 'likes' of undefined. Why is that? Thanks
It doesn't show any error and like and unlike works if I change the function like this const isLiked = post && post.likes && post.likes.some((like) => like.id === user.id); but the problem is when I reload the page, the button is in unliked state even if the user has already liked the post.
Re: TypeError, it might be that we are accessing it too early before post exists? post would be the object that is undefined in that error. I assumed the post was already loaded, which is why I was using initialPost. Idk but you can use optional chaining const isLiked = post?.likes.some((like) => like.id === user?.id);
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.