georstat/react-native-image-cache

Component renders twice

Closed this issue ยท 13 comments

Hey,
there is an unnecessary addtional rerender happening on the component and caused by the useEffect
image

specifically by uri in deps array

}, [propsSource, uri, propsOptions]);

the problem is when my propsSource is an https for image, the uri will never be equal to propsSource because the uri is the local path while propsSource is an http url, therefore entering to the first if and redo the work again

it would cause an infinite loop if

didnt save it, because the second time it enters to setUrl, the url will be the same as the previous one so it won't rerun the useEffect again.

Also please notice the performance difference between the first log and the second one, if we could make it like the second one it will be super fast and unnoticable.

@saif-o99 thanks for spotting this, please feel free to open a PR that solves it! Thanks

@efstathiosntonas i can do that, can you just tell if the uri in the deps array is needed? if yes, what for.

also is this code still needed?
https://github.com/georstat/react-native-image-cache/blob/5367b6f5cb6f552a04a1c8359e555bfdacef0133/src/CachedImage.tsx#L31C14-L31C14

function useStateIfMounted<S>(

if these were added for the component unmounted warning then we no longer need them

@saif-o99 I really can't remember ๐Ÿ˜„ , it's kindof translation of this one: https://github.com/wcandillon/react-native-expo-image-cache/blob/b81b99b01918558c7680595980d76003d507bc6f/src/Image.tsx#L53. I'm rereading the flow now to remember it.

About the useStateIfMounted it's needed for react < 18 or Paper builds with react > 17.

@saif-o99 I really can't remember ๐Ÿ˜„ , it's kindof translation of this one: https://github.com/wcandillon/react-native-expo-image-cache/blob/b81b99b01918558c7680595980d76003d507bc6f/src/Image.tsx#L53. I'm rereading the flow now to remember it.

i traced the code but couldnt find a case where we need to recall load when the url changes so i think we dont need it. also the first if statement in the useEffect will never match because propsSource is http url while uri is local path so this needs to be removed too

i think this should be the proper flow:

  useEffect(() => {
    load(props).catch();
    if (propsSource !== currentSource) {
      setCurrentSource(propsSource);
      setUri(undefined);
      resetAnimations();
    }
    /* eslint-disable react-hooks/exhaustive-deps */
  }, [propsSource, propsOptions]);

I believe this line setUri(undefined); is the reason of the check if (propsSource !== uri) {

maybe the propsSource should be currentSource, running some tests now.

i think this should be the proper flow:

  useEffect(() => {
    load(props).catch();
    if (propsSource !== currentSource) {
      setCurrentSource(propsSource);
      setUri(undefined);
      resetAnimations();
    }
    /* eslint-disable react-hooks/exhaustive-deps */
  }, [propsSource, propsOptions]);

100%, i'll make a PR for that

I'm checking if propsSource should be currentSource, give me 5 mins, check it your self too

I believe this line setUri(undefined); is the reason of the check if (propsSource !== uri) {

maybe the propsSource should be currentSource, running some tests now.

i think you meant url to be currentSource

because
url is something like /data/user/0/com.app/cache/images_cache/80f56239124b6e6fa6505bc87e88cc801da0a195.jpg
while currentSource and propsSource is something like https://app/api/6253388ccdf378256ede6aae/picture
so url would never be equal to currentSource or propsSource... correct me if im wrong

@saif-o99 can you test this out:

  const prevSource = useRef<string | null>(propsSource);


  useEffect(() => {
    if (prevSource.current !== uri) {
      load(props).catch();
    }
    if (propsSource !== currentSource) {
      setCurrentSource(propsSource);
      setUri(undefined);
      resetAnimations();
    }
    /* eslint-disable react-hooks/exhaustive-deps */
  }, [propsSource, uri, propsOptions, prevSource]);

  const load = async ({
    maxAge,
    noCache = false,
    onError,
    options = {},
    source
  }: ImageProps): Promise<void> => {
    if (source) {
      try {
        const path = await CacheManager.get(source, options, noCache, maxAge).getPath();

        if (path) {
          setUri(path);
          prevSource.current = path; <---- ADDED
          setError(false);
        } else {
          setError(true);
          prevSource.current = null; <----- ADDED
          onError({
            nativeEvent: { error: new Error("Could not load image") }
          });
        }
      } catch (e: any) {
        setError(true);
        onError({ nativeEvent: { error: e } });
      }
    }
  };

@efstathiosntonas there is a unneeded complexity with this code, here is the flow
1- enters useEffect then enters the load function
2- load function will setUri and prevSource
3- prevSource won't trigger rerender on useEffect so no need for it there
4- uri will trigger a rerender on useEffect but not the IF statement because prevSource.current == uri
5- but then setUri(undefined) will run and trigger rerender on useEffect and now it will enter the IF statement because uri changed

my observation is this:
1- if propsOptions changed but the source still the same the prevSource.current !== uri will prevent enter to load function again
2- prevSource won't trigger useEffect rerender
3-still not sure we need uri as deps array item, i think we dont need it
4-currentSource should be useRef not useState

and this is my latest code change please check it

  const currentSource = React.useRef<string>(propsSource);
  
  useEffect(() => {
    load(props).catch();
    if (propsSource !== currentSource.current) {
      currentSource.current = propsSource;
      resetAnimations();
    }
    /* eslint-disable react-hooks/exhaustive-deps */
  }, [propsSource, propsOptions]);

  const load = async ({
                        maxAge,
                        noCache = false,
                        onError,
                        options = {},
                        source,
                      }: ImageProps): Promise<void> => {
    if (source) {
      try {
        const path = await CacheManager.get(
          source,
          options,
          noCache,
          maxAge
        ).getPath();

        if (path) {
          setUri(path);
          setError(false);
        } else {
          setUri(undefined); <=== ADDED
          setError(true);
          onError({
            nativeEvent: { error: new Error('Could not load image') },
          });
        }
      } catch (e: any) {
        setUri(undefined); <=== ADDED
        setError(true);
        onError({ nativeEvent: { error: e } });
      }
    }
  };

@saif-o99 looks good to me, submit it ๐Ÿ˜„

closed via 2.8.0