'React custom Image Component doesn't render based on given condition

I am trying to build an image component in ReactJS with a fallback prop if provided will render the fallback component until the image has been downloaded and rendered properly.

The conditional rendering is as follows :

if (loading) {
    return fallback;
  } else if (error) {
    return <span>ERROR</span>;
  } else if (src) {
    return (
      <>
        <img alt="" ref={imageRef} decoding="async" src={props.src} />
      </>
    );
  } else return null;
}

And the Image is called in the App.js file as

<Image
  src="https://source.unsplash.com/random/100x100"
  fallback={<h1>Loading...</h1>}
/>

I'm using useEffect hook to call function that further checks if the image has been mounted to the screen using ref.

React.useLayoutEffect(() => {
    imageRef.current = true;
    start();
    return () => {
      imageRef.current = false;
    };
  }, []);

  async function start() {
    if (!src || !fallback) {
      const errorMessage = "`src` & `fallback` must be provided";
      setError(errorMessage);
      return;
    }
    setLoading(true);
    tryLoadImage();
  }

  async function loadImage() {
    const img = imageRef.current;
    if (!img) {
      return;
    }

    setLoading(false);
  }

  async function tryLoadImage() {
    try {
      await loadImage();
      if (imageRef.current) {
        setLoading(false);
      }
    } catch (error) {
      console.log("error", error);
    }
  }

The problem I'm facing:

  1. Even after using conditional statements in the return part, the component gives a black-white space then renders the image, hence no such 'Loading...' is getting rendered on the screen.
  2. I have tried using setTimeOut for the delay in 'setLoading' prop, in this case, the fallback is getting rendered after that certain timeout delay not before.

Things I'm unsure about:

  1. Is this happening due to batch update in the useState hook?
  2. My useEffect hook fires after the return gets painted to the screen. Is this mechanism correct in my usecase?

Kindly refer to Codesandbox link for the live demo/code : https://codesandbox.io/s/img-lazy-ke6zz

Any help in the right direction would be quite helpful.



Solution 1:[1]

You can use useEffect better in this case; you have a state loading. You should listen to when this value changes in your useEffect.

useEffect(() => {
  setLoading(false);
}, [loading]);

The value you pass in the array in the useEffect hook is getting listened to (there can be more than value in here, as it's an array). So when one of those values changes you run the code in useEffect. In this component loading should only changes once to false. So whenever you use setLoading in your code this useEffect is ran.

I recommend you read more about this hook here: https://reactjs.org/docs/hooks-effect.html

Also, I think it's better in this case, you default the loading state to true instead of false. So when the component renders it is loading from the start. In the start function you can use setLoading to false when src or fallback isn't set. This is also why your fallback isn't being shown. As you default loading to false and you show the fallback when this is loading is true.

A better practice of using React.useEffect and React.useState is importing it in your file. This can be done as followed import React, { useRef, useEffect, useState } from "react";. Then you can use them like this const [loading, setLoading] = useState(false); without React prefixing it.

You can find an edited version of your CodeSandbox here https://codesandbox.io/s/img-lazy-forked-l8zuf?file=/src/Image.js. I also changed the returning statement, which you can do if you don't want to show errors.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1