Danny
Danny

Reputation: 337

Button not changing state in react js based on api call

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.

Upvotes: 3

Views: 846

Answers (1)

Linda Paiste
Linda Paiste

Reputation: 42228

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

Upvotes: 2

Related Questions