Search code examples
reactjsreact-nativereact-componentreact-statereact-component-unmount

How do I clear out an Image.getSize request when unmounting a React Native component?


I have a React Native component that makes an Image.getSize request for each image in the component. Then within the callback of the Image.getSize requests, I set some state for my component. That all works fine, but the problem is that it's possible for a user to transition away from the screen the component is used on before one or more Image.getSize requests respond, which then causes the "no-op memory leak" error to pop up because I'm trying to change state after the component has been unmounted.

So my question is: How can I stop the Image.getSize request from trying to modify state after the component is unmounted? Here's a simplified version of my component code. Thank you.

const imgWidth = 300; // Not actually static in the component, but doesn't matter.

const SomeComponent = (props) => {
    const [arr, setArr] = useState(props.someData);

    const setImgDimens = (arr) => {
        arr.forEach((arrItem, i) => {
            if (arrItem.imgPath) {
                const uri = `/path/to/${arrItem.imgPath}`;

                Image.getSize(uri, (width, height) => {
                    setArr((currArr) => {
                        const newWidth = imgWidth;
                        const ratio = newWidth / width;
                        const newHeight = ratio * height;

                        currArr = currArr.map((arrItem, idx) => {
                            if (idx === i) {
                                arrItem.width = newWidth;
                                arrItem.height = newHeight;
                            }

                            return arrItem;
                        });

                        return currArr;
                    });
                });
            }
        });
    };

    useEffect(() => {
        setImgDimens(arr);

        return () => {
            // Do I need to do something here?!
        };
    }, []);

    return (
        <FlatList
            data={arr}
            keyExtractor={(arrItem) => arrItem.id.toString()}
            renderItem={({ item }) => {
                return (
                    <View>
                        { item.imgPath ?
                            <Image
                                source={{ uri: `/path/to/${arrItem.imgPath}` }}
                            />
                            :
                            null
                        }
                    </View>
                );
            }}
        />
    );
};

export default SomeComponent;

Solution

  • I had to implement something similar, I start by initialising a variable called isMounted.

    It sets to true when the component mounts and false to when the component will unmount.

    Before calling setImgDimens there's a check to see if the component is mounted. If not, it won't call the function and thus will not update state.

    const SomeComponent = (props) => {
      const isMounted = React.createRef(null);
      useEffect(() => {
        // Component has mounted
        isMounted.current = true;
    
        if(isMounted.current) {
          setImgDimens(arr);
        }
    
        return () => {
          // Component will unmount
          isMounted.current = false;
        }
      }, []);
    }
    

    Edit: This is the answer that worked for me, but for what it's worth, I had to move the isMounted variable to outside the SomeComponent function for it to work. Also, you can just use a regular variable instead of createRef to create a reference, etc.

    Basically, the following worked for me:

    let isMounted;
    
    const SomeComponent = (props) => {
        const setImgDimens = (arr) => {
            arr.forEach((arrItem, i) => {
                if (arrItem.imgPath) {
                    const uri = `/path/to/${arrItem.imgPath}`;
    
                    Image.getSize(uri, (width, height) => {
                        if (isMounted) { // Added this check.
                            setArr((currArr) => {
                                const newWidth = imgWidth;
                                const ratio = newWidth / width;
                                const newHeight = ratio * height;
    
                                currArr = currArr.map((arrItem, idx) => {
                                    if (idx === i) {
                                        arrItem.width = newWidth;
                                        arrItem.height = newHeight;
                                    }
    
                                    return arrItem;
                                });
    
                                return currArr;
                            });
                        }
                    });
                }
            });
        };
    
        useEffect(() => {
            isMounted = true;
            setImgDimens(arr);
    
            return () => {
                isMounted = false;
            }
        }, []);
    };