Home > Software engineering >  UseState not working at all with Object of Arrays
UseState not working at all with Object of Arrays

Time:01-10

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 videoIds 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]);
  • Related