Create a Lazy-Loading Image Component with React Hooks
Tags: react, javascript, 100daysofcode, webdev
Published: 2020-06-21 05:23:43

Lazy-loading images (like those in Medium or those created by *gatsby-image* 🧡) can sometimes add an extra touch of style to a page. To create such an effect, one will need A) a tiny version of the image for preview, ideally inlined as data URL, and B) the aspect ratio of the image to create a placeholder to prevent reflows. In this article, I will share how I created a lazy-loading image component with React Hooks.

[CodeSandbox Demo](https://codesandbox.io/s/create-a-lazy-loading-image-component-with-react-hooks-9hjbm?fontsize=14&hidenavigation=1&theme=dark)

> Hi, Dev.to Developers! Excited to be part of the DEV Community and this is my first post.

## First, the barebone - HTML/CSS 🦴

Usually, a lazing loading image consists of 4 HTML Elements:

```html
<div class="wrapper">
  <div style="padding-bottom:76%;"></div> 
  <img
    src="https://images.unsplash.com/photo-1518991791750-044b923256f0?fit=crop&w=25"
  />
  <img
    src="https://images.unsplash.com/photo-1518991791750-044b923256f0?fit=crop&w=1200"
    class="source"
  />
</div>
```

1. a relatively positioned wrapper `div`,
2. an intrinsic placeholder `div` for maintaining aspect ratio. It has padding-bottom with a percentage value(relative to the width of the containing block), e.g. for a 16:9 image, the percentage is calculated as 9/16 * 100% = 56.25%,
3. an absolutely positioned `img` for the tiny version of the image, also known as LQIP(**L**ow-**Q**uality **I**mage **P**laceholder), stretched to cover the wrapper. Data URL is usually used as the src to save HTTP requests,
4. an absolutely positioned `img` for the source image, placed on top of the LQIP, initialized with `opacity: 0`.

```css
.wrapper {
  position: relative;
  overflow: hidden;
}
img {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  object-fit: cover;
  object-position: center;
}
.source {
  opacity: 0;
  transition: opacity 1s;
}
.loaded {
  opacity: 1;
}
```

## Turn it into React Component ⚛

```jsx
import React, { useState, useEffect, useRef } from "react";
import clsx from "clsx"; // a utility for constructing className conditionally
function LazyImage({ className, src, alt, lqip, aspectRatio = 2/3 }) {
  const [loaded, setLoaded] = useState(false);
  const imgRef = useRef();
  useEffect(() => {
    if (imgRef.current && imgRef.current.complete) {
      setLoaded(true);
    }
  }, []);
  return (
    <div className={clsx("wrapper", className)}>
      <div style={{ paddingBottom: `${100 / aspectRatio}%` }} />
      <img src={lqip} aria-hidden="true" />
      <img
        loading="lazy"
        src={src}
        alt={alt}
        ref={imgRef}
        onLoad={() => setLoaded(true)}
        className={clsx("source", loaded && "loaded")}
      />
    </div>
  );
}
export default LazyImage;
```

Let's break it down: there is a loaded state to track the `loading` state of the souce image, initialized to be false. A "load" event listener is added to the source `img` element so when it finishes loading, the state is updated and a "loaded" class name is appended to its class list which sets its opacity to 1. In cases which the source [image has completely loaded](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/complete) before this component is mounted, the newly added "load" event listener will never fire. That's why a ref is also passed to the `img` element for checking its complete attribute on mount, and update the state accordingly.

Also, a `loading="lazy"` attribute is added to the source `img` to tell the browser to load the image immediately if it is in the viewport, or to fetch it when the user scrolls near it. More about that in this [web.dev article](https://web.dev/lazy-loading-images/). I also added `aria-hidden="true"` to the LQIP `img` to [hide it from the accessibility API](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-hidden_attribute).

## Usage

To use this component, you'll have to generate the image LQIP and get its aspect ratio. There are libraries that help you to integrate the generation into your build process, for example, [zouhir/lqip](https://github.com/zouhir/lqip). Apparently, if you're using Cloudindary, you can [create LQIP through their image transformation pipeline](https://cloudinary.com/blog/low_quality_image_placeholders_lqip_explained). But I suspect you can only get a regular URL instead of data URL or base64 so you might have to convert it yourself if you want to inline it.

In previous projects, I used [sharp](https://github.com/lovell/sharp)(a high-performance image processing module) in Next.js `getStaticProps` (a function that runs at build time for static generation) to help me populating those image data. Below is the function that I used:

```javascript
import got from 'got'; // HTTP request library for Node.js
import sharp from 'sharp';
sharp.cache(false);
async function generateLazyImage(src) {
  const { body } = await got(src, { responseType: 'buffer' });
  const sharpImage = sharp(body);
  const { width, height, format } = await sharpImage.metadata();
  const lqipBuf = await sharpImage
    .resize({ width: 30, height: 30, fit: 'inside' })
    .toBuffer();
  return {
    src,
    aspectRatio: width / height,
    lqip: `data:image/${format};base64,${lqipBuf.toString('base64')}`,
  };
}
```

That's it! This `<LazyImage />` is a pretty simple component that I use in almost all of my projects. Let me know your thoughts and how you present images on your sites. 😉

Please [follow my Twitter account](https://twitter.com/hangindev) if you want to read my future posts. I promise I will figure out how to do RSS with Next.js soon... (Updated on Jun25, 2020: there is RSS feed for my blog now. ✅)

![React Summit](https://dev-to-uploads.s3.amazonaws.com/i/2ub10uyfjmw681ixyaae.png)

> React Summit is coming back on October 15-16. There'll be speakers like [Kent C. Dodds](https://twitter.com/kentcdodds), [Max Stoiber](https://twitter.com/mxstbr), [Sara Vieira](https://twitter.com/nikkitaftw), [Sharif Shameem](https://twitter.com/sharifshameem).
> 
> Register for free before September 20: [https://ti.to/gitnation/react-summit?source=REFURCR-1](https://ti.to/gitnation/react-summit?source=REFURCR-1).