DEV Community

Cover image for How to detect images loaded in React
Alejandro Martinez
Alejandro Martinez

Posted on • Edited on

How to detect images loaded in React

When I performed a manual deep linking hook in a web application, the automatically scrolling down to a specific section caused a delay by loading of images.

How to detect the loading issues of the images before executing any action in react? The next hook uses eventListener with load and errorevents, and detects the HTMLImageElement.complete property of javascript, to determine if all images in a specific wrapper element have been completed.

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

export const useOnLoadImages = (ref: RefObject<HTMLElement>) => {
  const [status, setStatus] = useState(false);

  useEffect(() => {
    const updateStatus = (images: HTMLImageElement[]) => {
      setStatus(
        images.map((image) => image.complete).every((item) => item === true)
      );
    };

    if (!ref?.current) return;

    const imagesLoaded = Array.from(ref.current.querySelectorAll("img"));

    if (imagesLoaded.length === 0) {
      setStatus(true);
      return;
    }

    imagesLoaded.forEach((image) => {
      image.addEventListener("load", () => updateStatus(imagesLoaded), {
        once: true
      });
      image.addEventListener("error", () => updateStatus(imagesLoaded), {
        once: true
      });
    });

    return;
  }, [ref]);

  return status;
};
Enter fullscreen mode Exit fullscreen mode

Note: is important to add both load and error to avoid any blocking after load page.

According with the documentation of complete prop, the image is considered completely loaded if any of the following are true:

  • Neither the src nor the srcset attribute is specified. The srcset attribute is absent and the src attribute, while specified, is the empty string ("").
  • The image resource has been fully fetched and has been queued for rendering/compositing.
  • The image element has previously determined that the image is fully available and ready for use.
  • The image is "broken;" that is, the image failed to load due to an error or because image loading is disabled.

To use it you have to pass a ref wrapper to limit the search images.

import { useRef } from "react";
import { useOnLoadImages } from "./hooks/useOnLoadImages";
import "./styles.css";

export default function App() {
  const wrapperRef = useRef<HTMLDivElement>(null);
  const imagesLoaded = useOnLoadImages(wrapperRef);

  return (
    <div className="App" ref={wrapperRef}>
      <h2>How to detect images loaded in React</h2>
      <div>
        <p>{!imagesLoaded ? "Loading images..." : "Images loaded"}</p>
        <img src="https://source.unsplash.com/1600x900/?nature" alt="nature" />
        <img src="https://source.unsplash.com/1600x900/?water" alt="water" />
        <img src="https://source.unsplash.com/1600x900/?animal" alt="animal" />
        <img src="https://source.unsplash.com/1600x900/?lake" alt="lake" />
        <img src="https://source.unsplash.com/1600x900/?life" alt="life" />
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here there are a demo Link (reload internal browser)

If you like the article follow me in:

Top comments (4)

Collapse
 
adamnmth profile image
Adam Nemeth

Great article, thanks!

I was wondering if there is a specific reason to do a .map before the .every check?
images.map((image) => image.complete).every((item) => item === true)

wouldn't it be more simple to just check for images.every((item) => item.complete === true) instead, and save one extra loop?

Collapse
 
awenn2015 profile image
Alexander Antonov • Edited

Wouldn't it be easier to do it this way? and in general, you have some strange mechanics of work, when loading each element, you have to perform a million iterations of the cycle

Updated: Plus, after the tests, I want to add that this code will not even work since useEffect will not respond to ref, you need to change it to ref.current

import { RefObject, useEffect, useState } from 'react'

/**
 * @author awenn2015
 * @param ref
 * @param deps
 */
const useOnLoadImages = <T extends HTMLElement>(ref: RefObject<T>, deps?: any[]) => {
  const [isLoading, setLoading] = useState(true)
  const [isError, setError] = useState(false)

  function waitForLoad(img: HTMLImageElement) {
    return new Promise<void>((resolve, reject) => {
      if (img.complete) {
        return resolve()
      }

      img.onload = () => resolve()
      img.onerror = () => reject()
    })
  }

  const allDeps = [ref.current]
  if (deps?.length) allDeps.push(...deps)

  useEffect(() => {
    if (!ref?.current) return
    setError(false)
    setLoading(true)

    const promises = [...ref.current.querySelectorAll('img')]
      .map(it => waitForLoad(it))

    if (!promises.length) {
      setLoading(false)
      return
    }

    Promise.all(promises)
      // .then(() => {
      //   console.log('all images has been successfully loaded!')
      // })
      .catch((err: Error) => {
        console.error(err)
        setError(true)
      })
      .finally(() => {
        setLoading(false)
      })
  }, allDeps)

  return [isLoading, isError]
}

export default useOnLoadImages
Enter fullscreen mode Exit fullscreen mode
Collapse
 
aleksandrhovhannisyan profile image
Aleksandr Hovhannisyan

Note that this will run into a race condition if you set the src attribute on your images before you register their load listeners. I was able to reproduce this by refreshing the Codesandbox demo iframe; the images load very quickly because they were cached by the browser. Unfortunately, this means that by the time the event listener is registered, the load event will have already fired. So the message is never displayed.

See here for more context: stackoverflow.com/questions/146485....

Collapse
 
alejomartinez8 profile image
Alejandro Martinez • Edited

Yes you're right, I had to use it in an app when the content comes from a CMS and we parse it, and normally the images aren't cached. I'm going to review it on this case. Thanks.