exhaustive-deps rule doesn't recognise result of custom hook as React reference

Viewed 246

Imagine a hook:

export function useMounted() {
    const mounted = React.useRef<boolean>(false);
    React.useEffect(() => {
        mounted.current = true;
        return () => {
            mounted.current = false;
        };
    }, []);
    return mounted;
}

I'd like to use that hook to determine whether component is still mounted or not. One use case for it is to asynchronously load image and display image only when browser has fetched and cached it. Simplified version of such use case can be:

function useLazyImage(src: string, triggerCallback: any) {
    const mounted = useMounted();
    const image = useRef<HTMLImageElement>();

    useEffect(() => {
        if (!!src) {
            image.current = new Image();
            image.current.onload = () => {
                if (mounted.current)
                    triggerCallback();
            };
            image.current.src = src;
            return () => {
                image.current = undefined;
            };
        }
        return undefined;
    }, [src]);
}

It doesn't include error handling and doesn't handle changes of triggerCallback but it shows my problem. react-hooks/exhaustive-deps warns me about missing mounted dependency even though it shouldn't afaik. Since mounted is Ref, by using it I don't accidentally close over stale scope and eslint in fact knows this. If I change my code to

function useLazyImage(src: string, triggerCallback: any) {
    const mounted = React.useRef<boolean>(false);
    React.useEffect(() => {
        mounted.current = true;
        return () => {
            mounted.current = false;
        };
    }, []);
    const image = useRef<HTMLImageElement>();

eslint doesn't warn me anymore. Is this limitation of ESLint that it can't know what a result of my custom hook is or I'm doing this incorrectly and there's a bug lurking?

1 Answers

In your first example, you are returning the ref from the useMounted function in which it's created. So the reference is being leaked and could therefore be changed. In your bottom example, the reference is created as a function-local value and is therefore not part of your component's state. This is why you aren't seeing the warning with the bottom example.

Related