Search code examples
javascriptreact-nativereact-hooksinfinite-loop

React Native infinite loop with object array in useEffect


In my project, I need to get selected items from a Flatlist and pass them to my parent component.

I created a local state like this:

const [myState, setMyState] = useState<IStateType[] | []>([])  

Each time an item is selected I try to add it to my useEffect:

useEffect(() => {
    const result = myState.filter((el) => el.id !== item.id)
    if (isSelected) {
      setMyState([
        ...result,
        {
          propOne: 0,
          propTwo: 1,
          id: item.id,
         ...
        },
      ])
    } else {
      setMyState(result)
    }
  }, [isSelected])

But I would need to put mySate in the dependency of my useEffect to add each time the new items selected. If I add it to the useEffect dependency it causes an infinite loop ^^
How to add each new item to my array while listening to all the changes and without causing an infinite loop?


Solution

  • I believe the issue you're having it's because you're not separating the concerns of each component correctly, once you have to relay on the previous data every time, the useEffect can be tricky. But there are two solutions to your issue:

    Make use of useState callback function:

    The useState function can be used with a callback rather than a value, as follows:

        useEffect(() => {
        if (isSelected) {
          setMyState(prevState => [
            ...prevState,
            {
              propOne: 0,
              propTwo: 1,
              id: item.id,
             ...
            },
          ])
        } else {
          setMyState(result)
        }
      }, [isSelected])
    

    Best structure of your components + using useState callback function

    What I could see about your approach is that you (as you showed) seems to be trying to handle the isSelected for each item and the myState in the same component, which could be done, but it's non-ideal. So I propose the creation of two components, let's say:

    • <List />: Should handle the callback for selecting an item and rendering them.

    <List />:

    function List() {
      const [myState, setMyState] = useState([]);
    
      const isItemSelected = useCallback(
        (itemId) => myState.some((el) => el.id === itemId),
        [myState]
      );
    
      const handleSelectItem = useCallback(
        (itemId) => {
          const isSelected = isItemSelected(itemId);
    
          if (isSelected) {
            setMyState((prevState) => prevState.filter((el) => el.id !== itemId));
          } else {
            setMyState((prevState) => prevState.concat({ id: itemId }));
          }
        },
        [isItemSelected]
      );
    
      return (
        <div>
          <p>{renderTimes ?? 0}</p>
    
          {items.map((item) => (
            <Item
              item={item}
              onSelectItem={handleSelectItem}
              selected={isItemSelected(item.id)}
            />
          ))}
        </div>
      );
    }
    
    • <Item />: Should handle the isSelected field internally for each item.

    <Item />:

    const Item = ({ item, selected = false, onSelectItem }) => {
      const [isSelected, setIsSelected] = useState(false);
    
      useEffect(() => {
        setIsSelected(selected);
      }, [selected]);
    
      return (
        <div>
          <p>
            {item.name} is {isSelected ? "selected" : "not selected"}
          </p>
          <button onClick={() => onClick(item.id)}>
            {isSelected ? "Remove" : "Select"} this item
          </button>
        </div>
      );
    };
    

    Here's a codesnack where I added a function that counts the renders, so you can check the performance of your solution.