Search code examples
reactjsreact-nativereact-hooksreact-reduxredux-toolkit

Strange problems with useEffect and useSelector cause infinite loops


Now I do not understand why the data value obtained by useSelector is monitored by useEffect and useState is used to render the page in a closed loop.

Here's my code:

const data = useSelector((state: DeviceList) => {
  if (state.basicSetting !== null) {
    return state.basicSetting.filter(
      item =>
        item.deviceName.includes('ac') &&
        !item.deviceName.includes('bz'),
    );
  } else {
    return null;
  }
});

const [list, setList] = useState<DeviceDataOne[][]>([]);

useEffect(() => {
  if (data !== null) {
    let arr = [];
    for (var i = 0; i < (data as DeviceDataOne[]).length; i += 4) {
      arr.push((data as DeviceDataOne[]).slice(i, i + 4));
    }
    setList(JSON.parse(JSON.stringify(arr)));
  } else {
    setList([]);
  }
}, [data]);

useEffect(() => {
  console.log('list', list);
}, [list]);

useEffect(() => {
  console.log('data', data);
}, [data]);

My understanding of useEffect monitoring should be that when the data is changed, he will re-execute the code in useEffect. However, I actually found that such writing would lead to infinite re-rendering. Therefore, I can't understand why there is an endless loop.

I tried to comment the middle piece of code

useEffect(() => {
  if (data !== null) {
    let arr = [];
    for (var i = 0; i < (data as DeviceDataOne[]).length; i += 4) {
      arr.push((data as DeviceDataOne[]).slice(i, i + 4));
    }
    setList(JSON.parse(JSON.stringify(arr)));
  } else {
    setList([]);
  }
}, [data]);

But I don't understand why I can't write it like this or what causes such problems.


Solution

  • My understanding of useEffect monitoring should be that when the data is changed, he will re-execute the code in useEffect.

    Your understanding of the useEffect hook is correct, but you are not understanding that your useSelector hook is potentially returning a new array object reference each time the component renders. In other words, data is a new reference that triggers the useEffect hook to run. The useEffect hook enqueues a list state update. When React processes the list state update it triggers a component rerender. data is selected and computed from the Redux store. Repeat ad nauseam.

    You can help break the cycle by using an equality function with the useSelector hook. See Equality Comparisons and Updates.

    import { shallowEqual, useSelector } from 'react-redux';
    
    ...
    
    const data = useSelector((state: DeviceList) => {
      if (state.basicSetting !== null) {
        return state.basicSetting.filter(
          item =>
            item.deviceName.includes('ac') &&
            !item.deviceName.includes('bz'),
        );
      } else {
        return null;
      }
    }, shallowEqual); // <-- shallow equality instead of strict equality
    

    Another good solution is to create a memoized selector function from Reselect, and since you are using Redux-Toolkit, createSelector is re-exported from Reselect. (Reselect is maintained by the same Redux/Redux-Toolkit developers)

    It allows you to write the same computed data value but it memoizes the result so that if the selected state going into the selector hasn't changed, then the selector returns the same previously computed value. In other words, it provides a stable returned data value that will only trigger the useEffect when state actually updates and the selector computes a new value reference.

    However, it is a bit unclear why you are selecting state from the store and duplicating it locally into state. This is generally considered a React anti-pattern. You can compute the list value directly from the store.

    Example:

    const list = useSelector((state: DeviceList) => {
      if (state.basicSetting === null) {
        return null;
      }
      
      const data = state.basicSetting.filter(
        item =>
          item.deviceName.includes('ac') &&
          !item.deviceName.includes('bz'),
      ) as DeviceDataOne[];
    
      const list: DeviceDataOne[] = [];
      while (data.length) {
        list.push(data.splice(0, 4));
      }
      return list;
    });
    
    useEffect(() => {
      console.log('list', list);
    }, [list]);