Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React: How do you lazyload image from API response?

My website is too heavy because it downloads 200-400 images after fetching data from the server (Google's Firebase Firestore).

I came up with two solutions and I hope somebody answers one of them:

  • I want to set each img to have a loading state and enable visitors to see the placeholder image until it is loaded. As I don't know how many images I get until fetching data from the server, I find it hard to initialize image loading statuses by useState. Is this possible? Then, how?
  • How can I Lazy load images? Images are initialized with a placeholder. When a scroll comes near an image, the image starts to download replacing the placeholder.
function sample() {}{
  const [items, setItems] = useState([])
  const [imgLoading, setImgLoading] = useState(true)  // imgLoading might have to be boolean[]
  useEffect(() => {
    axios.get(url).
    .then(response => setItems(response.data))
  }, [])
  return (
    items.map(item => <img src={item.imageUrl} onLoad={setImgLoading(false)} />)
  )
}
like image 490
Watanabe.N Avatar asked Dec 06 '25 04:12

Watanabe.N


2 Answers

I would create an Image component that would handle it's own relevant states. Then inside this component, I would use IntersectionObserver API to tell if the image's container is visible on user's browser or not.

I would have isLoading and isInview states, isLoading will be always true until isInview updates to true.

And while isLoading is true, I would use null as src for the image and will display the placeholder.

Load only the src when container is visible on user's browser.

function Image({ src }) {
  const [isLoading, setIsLoading] = useState(true);
  const [isInView, setIsInView] = useState(false);
  const root = useRef(); // the container

  useEffect(() => {
    // sets `isInView` to true until root is visible on users browser

    const observer = new IntersectionObserver(onIntersection, { threshold: 0 });
    observer.observe(root.current);

    function onIntersection(entries) {
      const { isIntersecting } = entries[0];

      if (isIntersecting) { // is in view
        observer.disconnect();
      }

      setIsInView(isIntersecting);
    }
  }, []);

  function onLoad() {
    setIsLoading((prev) => !prev);
  }

  return (
    <div
      ref={root}
      className={`imgWrapper` + (isLoading ? " imgWrapper--isLoading" : "")}
    >
      <div className="imgLoader" />
      <img className="img" src={isInView ? src : null} alt="" onLoad={onLoad} />
    </div>
  );
}

I would also have CSS styles that will toggle the placeholder and image's display property.

.App {
  --image-height: 150px;
  --image-width: var(--image-height);
}

.imgWrapper {
  margin-bottom: 10px;
}

.img {
  height: var(--image-height);
  width: var(--image-width);
}

.imgLoader {
  height: 150px;
  width: 150px;
  background-color: red;
}

/* container is loading, hide the img */
.imgWrapper--isLoading .img {
  display: none;
}

/* container not loading, display img */
.imgWrapper:not(.imgWrapper--isLoading) .img {
  display: block;
}

/* container not loading, hide placeholder */
.imgWrapper:not(.imgWrapper--isLoading) .imgLoader {
  display: none;
}

Now my Parent component, will do the requests for all the image urls. It would also have its own isLoading state that when set true would display its own placeholder. When the image url's request resolves, I would then map on each url to render my Image components.

export default function App() {
  const [imageUrls, setImageUrls] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    fetchImages().then((response) => {
      setImageUrls(response);
      setIsLoading((prev) => !prev);
    });
  }, []);

  const images = imageUrls.map((url, index) => <Image key={index} src={url} />);

  return <div className="App">{isLoading ? "Please wait..." : images}</div>;
}

Edit flamboyant-kare-zz6qq

like image 73
bertdida Avatar answered Dec 07 '25 18:12

bertdida


There are libraries for this, but if you want to roll your own, you can use an IntersectionObserver, something like this:

const { useState, useRef, useEffect } = React;

const LazyImage = (imageProps) => {
  const [shouldLoad, setShouldLoad] = useState(false);
  const placeholderRef = useRef(null);

  useEffect(() => {
    if (!shouldLoad && placeholderRef.current) {
      const observer = new IntersectionObserver(([{ intersectionRatio }]) => {
        if (intersectionRatio > 0) {
          setShouldLoad(true);
        }
      });
      observer.observe(placeholderRef.current);
      return () => observer.disconnect();
    }
  }, [shouldLoad, placeholderRef]);

  return (shouldLoad 
    ? <img {...imageProps}/> 
    : <div className="img-placeholder" ref={placeholderRef}/>
  );
};

ReactDOM.render(
  <div className="scroll-list">
    <LazyImage src='https://i.insider.com/536a52d9ecad042e1fb1a778?width=1100&format=jpeg&auto=webp'/>
    <LazyImage src='https://www.denofgeek.com/wp-content/uploads/2019/12/power-rangers-beast-morphers-season-2-scaled.jpg?fit=2560%2C1440'/>
    <LazyImage src='https://i1.wp.com/www.theilluminerdi.com/wp-content/uploads/2020/02/mighty-morphin-power-rangers-reunion.jpg?resize=1200%2C640&ssl=1'/>
    <LazyImage src='https://m.media-amazon.com/images/M/MV5BNTFiODY1NDItODc1Zi00MjE2LTk0MzQtNjExY2I1NTU3MzdiXkEyXkFqcGdeQXVyNzU1NzE3NTg@._V1_CR0,45,480,270_AL_UX477_CR0,0,477,268_AL_.jpg'/>
  </div>,
  document.getElementById('app')
);
.scroll-list > * {
  margin-top: 400px;
}

.img-placeholder {
  content: 'Placeholder!';
  width: 400px;
  height: 300px;
  border: 1px solid black;
  background-color: silver;
}
<div id="app"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

This code is having them load as soon as the placeholder is visible on the screen, but if you want a larger detection margin, you can tweak the rootMargin option of the IntersectionObserver so it starts loading while still slightly off screen.

like image 38
Jacob Avatar answered Dec 07 '25 20:12

Jacob