1

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;
HartleySan
  • 7,404
  • 14
  • 66
  • 119

1 Answers1

1

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;
        }
    }, []);
};
HartleySan
  • 7,404
  • 14
  • 66
  • 119
Dan
  • 8,041
  • 8
  • 41
  • 72
  • Clever and makes perfect sense. This sort of thing always feels really kludgy to me and makes me feel like the framework itself should better manage this sort of thing, but all the same, that works! Thank you. – HartleySan Mar 24 '20 at 16:19
  • Quick question, Dan: Why do you create a React ref and use `.current` instead of just a regular state variable that you update to false when the component unmounts? – HartleySan Mar 24 '20 at 16:20
  • I agree with you and being totally honest, I'm not sure if this is the best practice but it was something that worked effortlessly. In this post, it's documented that this is somewhat an anti-pattern https://github.com/facebook/react/issues/12111#issuecomment-361254441 – Dan Mar 24 '20 at 16:21
  • Thanks again, Dan. Any comment on why you used `createRef` instead of `useState` for the `isMounted` variable? Thanks. – HartleySan Mar 24 '20 at 17:17
  • @HartleySan nice edit, if you declare the variable inside of the component, you are reminded that assignment to the variable inside of the `useEffect` hook will be lost on each render. Using a `ref` helps mitigate that. But also placing the variable outside does the job too – Dan Mar 25 '20 at 10:29
  • Dan, that's exactly the problem I was having. The component was continually re-rendering each time image dimensions were calculated and the state was set for a particular image, which was consistently cause the render function to be called again. For whatever reason, when I did what you recommended, the reference would also be reset every time, and it just wouldn't work. I don't know why. All the same, simply moving the `isMounted` reference/variable outside the function solved all my problems. Again, I wish React (Native) wasn't designed the way it is, but oh well, I guess. – HartleySan Mar 25 '20 at 11:24