I have a useState
object that is declared like this at the start of the file.
const [comments, setComments] = useState({
step_up: [], toe_walking: [],
toe_touches: [], squat: [],
side_to_side: [], rolling: [],
leg_lifts: [], hand_to_knees: [],
floor_to_stand: [], chair_elevation: [],
jumping_jacks: [], jump_rope: [],
bear_crawl: []
})
And through a recoil state, I have a plethora of comment
objects. I want to assign each comment to the respective topic. A step_up
comment would go into the step_up
array inside the comments
object. I do this through a useEffect
, but absolutely nothing happens.
useEffect(() => {
let allComments = selectedClient.plan.comments
for( let i = 0; i < selectedClient.plan.comments.length; i ) {
let tag = allComments[i].videoId
console.log(tag)
if (comments[tag]) {
let newArr = [...comments[tag]]
newArr.push(allComments[i])
let newObj = { ...comments }
newObj[tag] = newArr
setComments(comments => ({ ...newObj }))
}
}
setLoading(false)
}, [selectedClient])
My console confirms that all of the videoId
s are valid, as the logs show the follows:
LOG step_up
LOG step_up
LOG bear_crawl
LOG bear_crawl
LOG jumping_jacks
LOG jump_rope
LOG hand_to_knees
LOG side_to_side
LOG chair_elevation
LOG floor_to_stand
LOG hand_to_knees
LOG chair_elevation
LOG squat
LOG leg_lifts
LOG rolling
LOG toe_touches
LOG toe_walking
LOG step_up
LOG step_up
LOG step_up
LOG step_up
LOG step_up
Whenever I throw a console.log(comments)
in there, it shows me that the whole object is just filled with empty arrays. The most confusing part, is that each array has exactly 1 comment inside of it by the end of the process. But I have no idea how those comments got in there, because every time I log comments it comes up as having nothing but empty arrays. What gives???
CodePudding user response:
Issue
The issue is that the comments
state that is referenced repeatedly is a stale state closure over the initial state. Each loop overwrites the previous loops enqueued state update.
useEffect(() => {
let allComments = selectedClient.plan.comments
for( let i = 0; i < selectedClient.plan.comments.length; i ){
let tag = allComments[i].videoId
console.log(tag)
if (comments[tag]) { // <-- (1) stale closure over initial state
let newArr = [...comments[tag]] // <-- (2) spreads empty array
newArr.push(allComments[i])
let newObj = { ...comments } // <-- (3) shallow copy initial state object
newObj[tag] = newArr
setComments(comments => ({ // <-- (4) shallow copy copy of state
...newObj
}))
}
}
setLoading(false)
}, [selectedClient])
The last enqueued state update is the one you finally see when the component rerenders.
Solution
It is suggested to use a functional state update to correctly update from any previous state instead of using whatever is closed over in callback scope.
useEffect(() => {
selectedClient.plan.comments.forEach(comment => {
const { videoId } = comment;
// Enqueue functional state update
setComments(comments => {
if (comments[videoId]) {
// State has matching property value, update state
return {
...comments,
[videoId]: comments[videoId].concat(comment);
};
} else {
// Nothing to update, return existing state
return comments;
}
});
});
setLoading(false);
}, [selectedClient]);