Search code examples
javascriptarraysreactjsreact-lifecycle

Array.includes returning conflicting results on react component load


I have a Profile component that, on load, calls a Django server to grab an array of IDs representing bookmarked profiles. If the bookmarks array contains the ID of the profile that the user is on (i.e. if the user is on /profile/1 and if [1, 4, 6].includes(1)), then an img on the page should be filled, otherwise it should just be outlined. The user can click the img, which then makes a call to the backend to add that ID to the array. Clicking it again removes it from the array.

However, because of some rendering order that I don't understand, that img is not switching to the filled version properly. Screenshot Here you can see the devtools info for the InfoCard component, a child of Profile. You can also see that profile 1 is in the bookmarks array prop that I pass into InfoCard, and that isFavourite is also true. isFavourite is the result of the includes() method that I perform in the parent, Profile. But the img (the pink bookmark outline next to the name) is not filled. You can also see I'm logging false thrice before I log true. That console log is in the render() method of InfoCard: console.log(this.props.isFavourite);.

Here's how I've programmed all of that:

Profile.js

  const [bookmarks, setBookmarks] = useState([]);
  const [isFavourite, setIsFavourite] = useState(false);

  const isUser = checkUser(
    localStorage.getItem("CONNECTORY_userId"),
    match.params.userId
  );

  const changeId = parseInt(localStorage.getItem("CONNECTORY_changeId"));

  useEffect(() => {
    async function fetchBookmarks() {
      const bookmarksData = await fetch(
        `${config.url}/profile/favouriteProfiles/${changeId}`
      ).then((res) => res.json());
      const reducedBookmarksData = bookmarksData.map(({ id }) => id);
      setBookmarks(reducedBookmarksData);
      const isFavouriteCheck =
        reducedBookmarksData.length > 0
          ? reducedBookmarksData.includes(parseInt(match.params.userId, 10))
          : false;
      setIsFavourite(isFavouriteCheck);
    }
    fetchBookmarks();
  }, [match.params.userId, history, changeId, isFavourite]);

  const handleChildBookmarkClick = async (id) => {
    await fetch(`${config.url}/user/favourites/${changeId}/${id}`);
  };

  return (
    <>
        {profileData && (
          <InfoCard
            data={profileData}
            experienceData={
              profileData.experience ? profileData.experience[0] : null
            }
            isUser={isUser}
            isFavourite={isFavourite}
            bookmarks={bookmarks}
            handleBookmarkClick={handleChildBookmarkClick}
          />
        )}
    </>
  );

InfoCard.js

export default class InfoCard extends Component {
  constructor(props) {
    super(props);
    this.state = {
      favourite: this.props.isFavourite,
    };
  }

  componentDidMount = () => {
    this.setState({ favourite: this.props.isFavourite });
  };

  handleBookmarkClick = () => {
    if (this.props.isUser) {
      return;
    }
    this.props.handleBookmarkClick(this.props.data.id);
    this.setState((prevState) => ({ favourite: !prevState.favourite }));
  };

  render() {
    console.log(this.props.isFavourite);
  ...
  {!this.props.isUser && (
     <button
       className="bookmark-container"
       onClick={this.handleBookmarkClick}
     >
       <img
         className="info-card-bookmark"
         src={this.state.favourite ? bookmarkFull : bookmarkEmpty}
         alt="Add this profile to your bookmarks"
       />
     </button>
   )}
  ...

Am I rendering things in the right order? I think it could also have something to do with asynchronous functions, on which I'm also a little weak.


Solution

  • The below code:

    const handleChildBookmarkClick = async (id) => {
     await fetch(`${config.url}/user/favourites/${changeId}/${id}`);
    };
    

    Isn't doing anything when the request succeeds. If I understood correctly it should be calling setIsFavourite or somehow trigger fetchBookmarks so that Profile refetches the data and updates the state (this seems like an overkill).

    Also below code looks fishy:

    componentDidMount = () => {
     this.setState({ favourite: this.props.isFavourite });
    };
    

    You should directly use props like this.props.isFavourite or use getDerivedStateFromProps. Cloning props into state isn't generally a good idea since it disconnects the parent-child relationship and duplicates state.
    For example in this case you ended up not updating isFavourite state of Profile. isFavourite should only exist in one place.