How to handle an unchanging array in useEffect dependency array?

Viewed 172

I have a component like this. What I want is for the useEffect function to run anytime myBoolean changes.

I could accomplish this by setting the dependency array to [myBoolean]. But then I get a warning that I'm violating the exhaustive-deps rule, because I reference myArray inside the function. I don't want to violate that rule, so I set the dependency array to [myBoolean, myArray].

But then I get an infinite loop. What's happening is the useEffect is triggered every time myArray changes, which is every time, because it turns out myArray comes from redux and is regenerated on every re-render. And even if the elements of the array are the same as they were before, React compares the array to its previous version using ===, and it's not the same object, so it's not equal.

So what's the right way to do this? How can I run my code only when myBoolean changes, without violating the exhaustive-deps rule?

I have seen this, but I'm still not sure what the solution in this situation is.

const MyComponent = ({ myBoolean, myArray }) => {
  const [myString, setMyString] = useState('');

  useEffect(() => {
    if(myBoolean) {
      setMyString(myArray[0]);
    }
  }, [myBoolean, myArray]
}
2 Answers

Solution 1

If you always need the 1st item, extract it from the array, and use it as the dependency:

const MyComponent = ({ myBoolean, myArray }) => {
  const [myString, setMyString] = useState('');

  const item = myArray[0];

  useEffect(() => {
    if(myBoolean) {
      setMyString(item);
    }
  }, [myBoolean, item]);
}

Solution 2

If you don't want to react to myArray changes, set it as a ref with useRef():

const MyComponent = ({ myBoolean, myArray }) => {
  const [myString, setMyString] = useState('');
  const arr = useRef(myArray);

  useEffect(() => { arr.current = myArray; }, [myArray]);

  useEffect(() => {
    if(myBoolean) {
      setMyString(arr.current);
    }
  }, [myBoolean]);
}

Note: redux shouldn't generate a new array, every time the state is updated, unless the array or it's items actually change. If a selector generates the array, read about memoized selectors (reselect is a good library for that).

I have an idea about to save previous props. And then we will implement function compare previous props later. Compared value will be used to decide to handle function change in useEffect with no dependency.

It will take up more computation and memory. Just an idea.

Here is my example:

function usePrevious(value) {
    const ref = useRef();
    useEffect(() => {
        ref.current = value;
    });

    return ref.current;
}

function arrayEquals(a, b) {
    return Array.isArray(a) &&
        Array.isArray(b) &&
        a.length === b.length &&
        a.every((val, index) => val === b[index]);
}

const MyComponent = ({ myBoolean, myArray }) => {
    const [myString, setMyString] = useState('');
    const previousArray = usePrevious(myArray);
    const previousBoolean = usePrevious(myBoolean);

    const handleEffect = () => {
        console.log('child-useEffect-call-with custom compare');

        if(myBoolean) {
            setMyString(myArray[0]);
        }
    };

    //handle effect in custom solve
    useEffect(() => {
        //check change here
        const isEqual = arrayEquals(myArray, previousArray) && previousBoolean === myBoolean;

        if (!isEqual)
            handleEffect();
    });

    useEffect(() => {
        console.log('child-useEffect-call with sallow compare');

        if(myBoolean) {
            setMyString(myArray[0]);
        }
    }, [myBoolean, myArray]);

    return myString;
}

const Any = () => {
    const [array, setArray] = useState(['1','2','3']);
    console.log('parent-render');

    //array is always changed
    // useEffect(() => {
    //     setInterval(() => {
    //         setArray(['1','2', Math.random()]);
    //     }, 2000);
    // }, []);


    //be fine. ref is not changed.
    // useEffect(() => {
    //     setInterval(() => {
    //         setArray(array);
    //     }, 2000);
    // }, []);


    //changed ref but value in array are not changed -> handle this case
    useEffect(() => {
        setInterval(() => {
            setArray(['1','2', '3']);
        }, 2000);
    }, []);

    return <div> <MyComponent myBoolean={true} myArray={array}/> </div>;
}
Related