Search code examples
react-native

React Native Prevent child re-render


I tried a bunch of different variations of useCallback and useMemo, as well as React.memo, but it doesn't have the behavior I want, can anyone help me? The problem is that whenever the state of the parent is changed (like when I turn the phone side ways, or when "isShown" changes) it re-renders the child and re-triggers the read-data function which not only waste data because it causes another call to the backEnd, but the user also has to wait for the data to be fetched again. Is there a way to not re-trigger handleReadData of the child each time the parent is re-rendered?

parent :

export default function Parent({}) {

const [isShown, setIsShown] = React.useState(false);

{...}

const WifiModuleSection = ({title, wifiModule, wifiSection}) => {
    return (
      <View style={styles.sectionWifiModuleContainer}>
          <View>
            {...}
          </View>
          <WifiModuleControls wifiModule={wifiModule} t={t} i18n={i18n} />
        </View>
    );
  };

return return (
                    <View>
                      {WifiModules.map(wifiModule => {
                       return (
                        <View style={{display:isShown ? 'flex' : 'none'}}>
                         <WifiModuleSection
                          title={wifiModule.name}
                          wifiModule={wifiModule}
                          wifiSection={wifiSection}
                         />
                        </View>
                       );
                      })}
                      {...}
                    </View>
                );
}

child :

const WifiModuleControls = ({wifiModule, t, i18n}) => {

{...}

const handleReadData = async () => {
    //Request Read state-data
    try {
      setLoadingReadData(true);
      const formData = {...};
      const res = await client.post('/read-data', formData);

      if (res.data.success) {
        {...}
        setData(res.data.data);
        setLoadingReadData(false);
      } else {
        Toast.showShortBottom('Error: ' + res.data.message);
        setLoadingReadData(false);
      }
    } catch (error) {
      Toast.showShortBottom('Error : ' + error);
      setLoadingReadData(false);
    }
  };

  useEffect(() => {
      handleReadData();
  }, []);

if (loadingReadData) {
    return (
      <ActivityIndicator
        style={{marginBottom: 1}}
        color={currentMainThemeColor}
        size={60}
      />
    );
  }

 return (
    <View>
    {...}
    </View>
 );
};

export default WifiModuleControls;

I simplified code to only show relevent parts


Solution

  • This isn't about preventing re-renders. It's either about:

    1. Preventing "unmounting/remounting" OR
    2. Caching data OR
    3. Lifting state up (similar to 2, but not the exact same)

    The problem is WifiModuleControls owns the fetching of data and the holding of it in state so that when it unmounts, that data is then "lost" and the only way to get it back is to refetch it when your component mounts again.

    This happens because of this line:

                          {isShown &&
                            WifiModules.map(wifiModule => {
                              return (
                                <WifiModuleSection
                                  title={wifiModule.name}
                                  wifiModule={wifiModule}
                                  wifiSection={wifiSection}
                                />
                              );
                            })}
    

    If isShown is false, then the components unmount and the data is gone. When isShown is true again, the components mount again and the only way to get their data back to refetch it all again.

    You can do several things to resolve your issue:

    1. Always keep the component mounted, just maybe make it invisible
    2. Move the fetching and state to the parent component and pass the fetched data into the WifiModuleSection. This way, when WifiModuleSection unmounts, the data is still held in the parent component.
    3. Cache the fetched data - this is where libraries like useSWR or tanstack-query shine brightest.

    EDIT:

    Option 1: I'm relatively naive to react-native, but if you wanted to keep everything else the same, instead of conditionally rendering based on isShown, the below would probably work:

    {WifiModules.map(wifiModule => {
        return (
          <View style={{display:isShown ? 'flex' : 'none'}}>
            <WifiModuleSection
              title={wifiModule.name}
              wifiModule={wifiModule}
              wifiSection={wifiSection}
            />
          </View>
        );
    })}
    

    This means that you'll have to get rid of the conditional rendering using isShown - so the whole snippet looks like this:

    {isShown && // remove this line entirely
        WifiModules.map(wifiModule => {
          return (
            <View style={{display:isShown ? 'flex' : 'none'}}> // replace with this conditional
                <WifiModuleSection
                  title={wifiModule.name}
                  wifiModule={wifiModule}
                  wifiSection={wifiSection}
                />
            </View>
          );
        })}