React lazy image loading and TypeScript — No more slow and broken images

React lazy image loading and TypeScript — No more slow and broken images

Creating a better UX is not as simple as it looks. Every component on-page matters. While working on a complex piece of code, we almost forgot about the simplest thing, a broken image.

Here in this tutorial, I will explain how you can create a simple Image component without breaking existing code. This component will support lazy loading and broken images.

brand Photo by ©Joanna Kosinska

1. Create an Image Component

To start with, First Create a folder in your component library and Create a file named index.tsx.

mkdir -p components/Image
touch components/Image/index.tsx

Add below line to index.tsx.

// components/Image/index.tsx

import React from "react";
interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {}
export default ({ ...props }: ImageProps) => {
  return <img {...props} />;
};

Note: I am using typescript to build this component. You can just remove typing if your project does not support typescript.

2. How to use Image in App Page

You can consume the Image component just like img HTML element. Since this image component extends HTMLImageElement. It supports all the Image properties.

// App.tsx

import React from "react";
import Image from "./components/Image";

function App() {
  return (
    <div className="App">
      <section className="section">
        <div className="container">
          <h1 className="title">React Components Demo</h1>
          <p className="subtitle">Lazy Image</p>
          <Image
            style={{ width: 400, height: 300 }}
            src="https://upload.wikimedia.org/wikipedia/commons/f/ff/Pizigani_1367_Chart_10MB.jpg"
          />
          <br />
          <Image src="https://doesnot.exits.com/image.png" />
        </div>
      </section>
    </div>
  );
}

export default App;

Try to reload the page, you will notice the image load very slowly. This is because the image is 10mb large. Same time the other image will be broken.

lazy_load_image1.png

3. Add a default image prop(placeholder)

Let's add a default image prop, modify the interface.

// components/Image/index.tsx

interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
  placeholderImg?: string;
}

Take placeholderImg image and show this to the user before the actual image loaded.

// components/Image/index.tsx

export default ({ placeholderImg, src, ...props }: ImageProps) => {
  const [imgSrc, setSrc] = useState(placeholderImg || src);
  return <img {...props} src={imgSrc} />;
};

No question will arise if we are showing a placeholder image, the how we will load the actual image.

thinking

4. Load Image Dynamically

For that, we will use the Image class from JavaScript. We will create an image dynamically and add an event listener to it.

// components/Image/index.tsx

export default ({ src, placeholderImg, ...props }: ImageProps) => {
  const [imgSrc, setSrc] = useState(placeholderImg || src);

const onLoad = useCallback(() => {
    setSrc(src);
  }, [src]);

  useEffect(() => {

    const img = new Image();
    img.src = src as string;
    img.addEventListener("load", onLoad);

    return () => {
      img.removeEventListener("load", onLoad);
    };
  }, [src, onLoad]);

  return <img {...props} alt={imgSrc} src={imgSrc} />;
};

Here we are using useEffect to create the image and load it in the background. We can listen to the load event using addEventListener. Once the image is loaded we can replace src with the actual src URL. Since the image has already loaded and cached to the browser. The next loading will be in no time.

Since we are adding event listeners to the image on load and error on mount of the component. We should remove on unmount of the component.

Let’s modify the App code.

// App.tsx

<Image
  style={{width: 400, height: 300 }} 
  placeholderImg="https://via.placeholder.com/400x200.png?text=This+Will+Be+Shown+Before+Load"
  src="https://upload.wikimedia.org/wikipedia/commons/f/ff/Pizigani_1367_Chart_10MB.jpg"
/>
<br />
<Image
  placeholderImg="https://via.placeholder.com/400x200.png?text=This+Will+Be+Shown+Before+Load"
  src="https://doesnot.exits.com/image.png"
/>

lazy_load_image2.png

If you notice, the second image is still showing the placeholder image. Since the image never loaded successfully so setSrc never been called.

5. Handle Error State

Let’s handle the image loading error. For that let’s add another prop errorImg and listen on error status.

// components/Image/index.tsx

import React, { useCallback, useEffect, useState } from "react";

interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
  placeholderImg?: string;
  errorImg?: string;
}

export default ({ src, placeholderImg, errorImg, ...props }: ImageProps) => {
  const [imgSrc, setSrc] = useState(placeholderImg || src);

  const onLoad = useCallback(() => {
    setSrc(src);
  }, [src]);

  const onError = useCallback(() => {
    setSrc(errorImg || placeholderImg);
  }, [errorImg, placeholderImg]);

  useEffect(() => {
    const img = new Image();
    img.src = src as string;
    img.addEventListener("load", onLoad);
    img.addEventListener("error", onError);
    return () => {
      img.removeEventListener("load", onLoad);
      img.removeEventListener("error", onError);
    };
  }, [src, onLoad, onError]);

  return <img {...props} alt={imgSrc} src={imgSrc} />;
};

Let’s modify the App code.

// App.tsx

import React from "react";
import Image from "./components/Image";

function App() {
  return (
    <div className="App">
      <section className="section">
        <div className="container">
          <h1 className="title">React Components Demo</h1>
          <p className="subtitle">Lazy Image</p>
          <Image
            style={{ width: 400, height: 300 }}
            placeholderImg="https://via.placeholder.com/400x200.png?text=This+Will+Be+Shown+Before+Load"
            src="https://upload.wikimedia.org/wikipedia/commons/f/ff/Pizigani_1367_Chart_10MB.jpg"
          />
          <br />
          <Image
            errorImg="https://image.shutterstock.com/image-vector/no-image-available-vector-illustration-260nw-744886198.jpg"
            placeholderImg="https://via.placeholder.com/400x200.png?text=This+Will+Be+Shown+Before+Load"
            src="https://doesnot.exits.com/image.png"
          />
        </div>
      </section>
    </div>
  );
}

export default App;

Conclusion: Now we have achieved broken image and lazy loading image both with the same component.

Note: You can use the placeholder image as a loader and show the loading image before loading the actual image. For that, you can use transition animation or Gif.

CodeSandbox Domo:

Thanks for your support. You can find all code in GitHub Repo: deepakshrma/react-components

Did you find this article valuable?

Support Deepak Vishwakarma by becoming a sponsor. Any amount is appreciated!