DEV Community

Cover image for Make Your Own Lazy Loading Image Component In React
Kunal Ukey
Kunal Ukey

Posted on

Make Your Own Lazy Loading Image Component In React

Introduction

We all have heard of lazy loading images, and some of you might have used them too.
It is as easy as adding a loading attribute to your image tag. Like below example

<img src="https://image-source.png" alt="image" loading="lazy" />
Enter fullscreen mode Exit fullscreen mode

But, Adding just a loading attribute to your image in React won't work because of client-side rendering.
So how to lazy load images in ReactJS?
Let's see.

Normal Use Case of Images In ReactJS

import { useEffect, useState } from "react";

function App() {
  const [photos, setPhotos] = useState(null);

  useEffect(() => {
    fetch("https://picsum.photos/v2/list")
      .then((res) => res.json())
      .then((data) => setPhotos(data));
  }, []);

  return (
    <div className="App">
      <div className="imageContainer">
        {photos &&
          photos.map((photo) => (
            <img
              key={photo.id}
              src={photo.download_url}
              alt="dummy-img"
              style={{ width: "100%" }}
            />
          ))}
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Above, I made a simple fetch request, mapped through image data, and rendered it into the browser.

Non-Lazy-Images After opening the network tab, you can see the huge amount of data consumed for the images we have yet to see in the viewport.

Solution:

We will use Intersection Observer API provided by the web browsers to monitor which images are in the viewport and which are not. This way, we will only request the images in the viewport 😀.

Let's start by creating a new component called LazyImage.js that will take props and handle all the logic.

Custom Lazy Loading Image Component

LazyImage.js

import { useRef, useState } from "react";

const LazyImage = ({
  placeholderSrc,
  placeholderClassName,
  placeholderStyle,
  src,
  alt,
  className,
  style,
}) => {
  const [isLoading, setIsLoading] = useState(true);
  const placeholderRef = useRef(null);

  return (
    <>
      {isLoading && (
        <img
          src={placeholderSrc}
          alt=""
          className={placeholderClassName}
          style={placeholderStyle}
          ref={placeholderRef}
        />
      )}
      <img
        src={src}
        className={className}
        style={isLoading ? {display: "none"} : style}
        alt={alt}
        onLoad={() => setIsLoading(false)}
      />
    </>
  );
};
export default LazyImage;
Enter fullscreen mode Exit fullscreen mode

Above, we have created a placeholder image that will immediately mount as the isLoading state is true. Then we applied an onLoad function to the actual image that will set the isLoading state to false so the placeholder image will get unmounted. When the isLoading state is true, we have to hide the actual image from showing, So we conditionally styled it by giving it a property of display: "none".

Placeholder-Images We have created a safe placeholder image to show until the image is completely loaded, which is good, as you can see in the above image.
But, still, all the images are being requested, which we don't want. We only want to request the images that are in the viewport. So now it's time to implement Intersection Observer 😀.

Final Code for Lazy Loading Image Component

LazyImage.js

import { useEffect, useRef, useState } from "react";

const LazyImage = ({
  placeholderSrc,
  placeholderClassName,
  placeholderStyle,
  src,
  alt,
  className,
  style,
}) => {
  const [isLoading, setIsLoading] = useState(true);
  const [view, setView] = useState("");
  const placeholderRef = useRef(null);

  useEffect(() => {
    // Initiating Intersection Observer
    const observer = new IntersectionObserver((entries) => {
      // Set actual image source && unobserve when intersecting
      if (entries[0].isIntersecting) {
        setView(src);
        observer.unobserve(placeholderRef.current);
      }
    });

    // observe for an placeholder image
    if (placeholderRef && placeholderRef.current) {
      observer.observe(placeholderRef.current);
    }
  }, [src]);

  return (
    <>
      {isLoading && (
        <img
          src={placeholderSrc}
          alt=""
          className={placeholderClassName}
          style={placeholderStyle}
          ref={placeholderRef}
        />
      )}
      <img
        src={view} // Gets src only when placeholder intersecting
        className={className}
        style={isLoading ? {display: "none"} : style}
        alt={alt}
        onLoad={() => setIsLoading(false)}
      />
    </>
  );
};
export default LazyImage;
Enter fullscreen mode Exit fullscreen mode

As you can see in the above code, we have initiated the intersection observer inside the useEffect hook, so it will apply only once the component is mounted.
Then we observe the placeholder image to check if it is intersecting (do note that on mount placeholder image will only occupy the space in the DOM.), and once the placeholder is intersecting, we will pass the src we accepted as a prop to the view state we created.
In this way, the actual image will start to make requests 🤩
Ultimately, we will unobserve the placeholder image to stop listening for the changes.

Lazy-Images Boom! 🔥
You can see we are only making requests for the images in the viewport.

I have created a package for you to use, so you don't have to make it yourself 😛 Visit HERE
Or run this npm command in your terminal.

npm install @kunalukey/react-image
Enter fullscreen mode Exit fullscreen mode

Video Tutorial:

Thanks for reading! ❤

Oldest comments (2)

Collapse
 
hkara107144 profile image
huseyinkara

Hi,which resource do you recommend to learn mern stack ?

Collapse
 
kunalukey profile image
Kunal Ukey

I would recommend you to try: roadmap.sh
and some youtube channels like Traversy Media, The Net Ninja, and FreeCodeCamp.