0

I have a parent component ParentItem which renders ChildItem components. ParentItem passes removeItem() to ChildItem components during render.

Normally, anytime useState is called and the new state is different from the previous state, the component should re-render.

For some reason, in this case it seems like removeItem() is passed to the children with childItemList at the time of render, so if one ChildItem calls removeItem() the next child will have stale a childItemList (ie they will have the initial childItemList with all children, when I want the previously removed child to be reflected in the following calls to removeItem(). I feel like the problem is that handleClick() is being called inside of panResponder which is useRef, but i'm not sure if thats really the case, and if it is why.

I have resorted to using a useRef copy of childItemList, make changes in the useRef copy and passing it to setChildItemList() within removeItem().

But I don't like how I need two variables to track the list of child components. I feel like there is probably a better way to go about this.

Parent:

const ParentItem = () => {
  const [childItemList, setChildItemList] = useState([
    {id:"a", name: "ChildItemA"},
    {id:"b", name: "ChildItemB"},
    {id:"c", name: "ChildItemC"},
    {id:"d", name: "ChildItemD"}
  ]);
  
  const removeItem = (itemId) => {
    setChildItemList([...items].filter(item => item.id !== itemId));
  }

  return(
    <View>
      {
        childItemList.map((item) => {
          return(
            <ChildItem
              key={Math.random()}
              handleClick={removeItem}
              details={item}
            />
          )
        })
      }
    </View>
  )
  
}
export default ParentItem;

Child:

const ChildItem = (props) => {
  
  const pan = useRef(new Animated.ValueXY()).current;
  
  const panResponder = useRef({
    PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {
        Animated.timing( pan, {
          toValue: { x: -10, y -10 },
          duration: 1000,
          useNativeDriver: false
        }).start(()=>{
          props.handleClick(props.details.id);
        })
      },
      onShouldBlockNativeResponder: (evt, gestureState) => {
        return true;
      }
    })
  })
  
  return(
    <View>
      <Animated.View
        {...panResponder.panHandlers}
      >
      </Animated.View>
    </View>
  );
  
}

export default ChildItem;

Parent: Current solution using useRef copy of childItemList

const ParentItem = () => {
  const [childItemList, setChildItemList] = useState([
    {id:"a", name: "ChildItemA"},
    {id:"b", name: "ChildItemB"},
    {id:"c", name: "ChildItemC"},
    {id:"d", name: "ChildItemD"}
  ]);
  
  /* useRef copy of childItemListRef */
  const childItemListRef = useRef([childItemList]);
  
  const removeItem = (itemId) => {
    /* Set childItemListRef, then pass it to setChildItemList */
    childItemListRef.current = childItemListRef.current.filter(item => item.id !== itemId);
    setChildItemList(childItemListRef);
  }

  return(
    <View>
      {
        childItemList.map((item) => {
          return(
            <ChildItem
              key={Math.random()}
              handleClick={removeItem}
              details={item}
            />
          )
        })
      }
    </View>
  )
  
}
export default ParentItem;
jjyj
  • 377
  • 4
  • 15

1 Answers1

0

First of, don't use a Math.random for key. You have the ID right there so use item.id instead since it is unique.

Also for the remove Item function, I would bind a unique function with the id in for every Child instead.

handleClick={() => removeItem(item.id)}

By doing this you should not need to have a useRef hook for this.

You have no use for useRef in the removeItem function either. Just go back to the first version of removeItem you where using.

const removeItem = (itemId) => {
    // This should be childItemList not just ...items
    setChildItemList([...childItemList].filter(item => item.id !== itemId));
}

This works fine for me. Here is a working example: https://codepen.io/nbjorling/pen/YzjVzvK

nbjorling
  • 94
  • 6
  • For sure, I quickly wrote up this example to just explain the problem I'm having for simplicity. Not using Math.random() in my original. I tried your suggestion but it looks like the the list of children are still stale. AFAIK when we are assigning removeItem() to handleClick, every call will refer to the initial copy of `childItemList` if we use useState – jjyj Jan 10 '23 at 01:30
  • I updated my answer. Also the children and the removeItem() function should not be stale, because when you update the state in the parent component, it will re-render the component and thus also creating the remoteItem functions again for each child. – nbjorling Jan 10 '23 at 15:44
  • So when I first ran into this issue that's what I was doing, but its wasn't working which is why I'm using `useRef` inside of `removeItem()` which fixes the problem (albiet redundant). I think because `removeItem()` is being called inside of `panResponder` which is a `useRef` inside the child component, its not re-rendering the parent, I'm just not sure if this is really the reason why `useState` in `removeItem()` isn't re-rendering the parent + children, and if so why, or if its something else. – jjyj Jan 11 '23 at 02:55
  • Hi again jjyj. I think I found why it didn't work in the first solution. thats because you tried to spread `...items` inside the `setChildiItemList` instead of `...childItemList`. Have a look at my updated answer. – nbjorling Jan 11 '23 at 08:55