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.
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.
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.