Extending Next.js' <Image /> component to improve UX
Tags: nextjs, javascript, ux, react
Published: 2021-11-22 14:09:55

If you have worked on Next.js, it's a good chance that you might have ended up using the **Image** component. 

While the Next's Image component already has a lot of built in features like blurring placeholder images when the images are loading or controlling image quality to improve the UX. 

In this article, we explore extending the Next's image component to improve the end user experience alternatively. 

## Plot

Here we address 2 main states when serving images

### 1. Loading state

Lazy loading images contributes to better UX since it helps in reducing the load time among other things, however, to improve it one step further we add something like an intermediate form of display until the image loads. For example, spinner or a skeleton loader

### 2. Errored state

What happens if the image url is incorrect or if the image service API is down for some reason? It would be ideal to have a fallback image so the end user has a seamless experience and doesn't end up seeing something like this. 

![Broken image example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6s4glcpnb84c2lbpsv4p.png) 

When using Next.js' Image component, it is important to wire it up with a fallback image because of [domains](https://nextjs.org/docs/api-reference/next/image#domains). 

After we've taken care of these 2 states, the solution ends up looking like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5vsmlr2y45eyjnposn5q.png) 
 
## ImageWithState

Let's dive in and extend the Next.js' <Image /> component to further support the aforementioned states.

Starting with the main imports

```tsx
import React from 'react'
import Image, { ImageProps } from 'next/image'
```

Now, we create a React component that simply extends the Next's Image component & also the types

```tsx

type ImageWithStateProps = ImageProps

function ImageWithState (props: ImageWithStateProps) {
  return <Image {...props} />
}
```

So far, we've done nothing other than adding a transparent abstraction over the Image component. The component **ImageWithState** will work same as Next's **Image**, only that the component name is different.

Let's now introduce the states

```tsx

function ImageWithState (props: ImageWithStateProps) {
  const [loading, setLoading] = React.useState(true)
  const [onErrorSrc, setOnErrorSrc] = React.useState<string | undefined>(undefined)
  
  return <Image {...props} />
}
```

When the component mounts, `loading` is set to true by default as the image would start loading

The onErrorSrc prop is the source url for the `fallback` image. The fallback image appears when the Image component throws an error. Let's go ahead and create the function to handle the errored state

```tsx

 function handleOnError (e: React.SyntheticEvent<HTMLImageElement, Event>): void {
    e?.currentTarget?.src !== props.fallback && setOnErrorSrc(props.fallback)
  }
  
```

This is triggered by the onError event 

```tsx
return <Image {...props} onError={(e) => handleOnError(e)} />
```

The handleOnError function is called when the <Image /> component would error. In that case we change the `src` prop of the element to the fallback image. 

Now, we manage the loading state

```tsx

  return (
    <div style={{ position: "relative" }}>
      {loading === true && (
        <SkeletonLoader
          style={{
            position: "absolute",
            zIndex: props.debug === "true" ? 99 : "auto"
          }}
          height={props.height}
          width={props.width}
        />
      )}
      <Image
        {...props}
        src={onErrorSrc || src}
        onLoadingComplete={() => !props.debug && setLoading(false)}
        onError={(e) => handleOnError(e)}
      />
    </div>
  );
```

To represent the loading state, I have used SkeletonLoader component. Feel free to use any other loading indicators like spinners or splash based on your use case. 

Furthermore, there is a `debug` prop which can be helpful when developing & styling to check if the loading indicators are styled appropriately. 

Mostly images are served from disk cache, in that case it becomes difficult to replicate a "loading" state for an image while developing. In such a situation, enabling the debug prop would provide a much efficient dev workflow than network throttling via the browser's dev tools.

If you haven't noticed yet, we pass the same `height` and `width` prop to the skeleton loader. This also further helps in avoiding layout shift as the space will be preserved for the image.

Finally, updating the type 

```tsx
type ImageWithStateProps = ImageProps & {
  fallback: string;
  debug?: string;
};
```

## Usage

Using the wrapper component should be same as using the Next's Image component. 

The **ImageWithState** component added 2 extra props which are for the fallback image in case of an error & a debug prop to help us make sure that the skeleton loader displays appropriately

Feel free to fork or play around this component on [CodeSandbox](https://codesandbox.io/s/next-image-with-state-f1icr)

You can also visit [https://f1icr.csb.app/](https://f1icr.csb.app/) to check out the working solution

## Caveats

**Build size**: These changes, including adding the svg skeleton loader library which is `react-skeleton-loader` adds approximately 5kB to your production build. But keep in mind that this component is reusable across your entire app so the build size won't bloat up further 

**Web vitals**: Next's Image component loads the images lazily by default. If you're loading the images [above the fold](https://www.abtasty.com/blog/above-the-fold/), remember to pass the [priority](https://nextjs.org/docs/api-reference/next/image#priority) prop so the page doesn't lose out on [LCP](https://web.dev/lcp/) points

**Blur**: It's possible to display a blurred image placeholder when the image is being loaded. This is supported by the `placeholder` property on the Image component.

**Unoptimized**: You may have seen an `unoptimized` true prop passed to the Image component. This is because I haven't setup any [loader configuration](https://nextjs.org/docs/api-reference/next/image#loader) for these images. Make sure to keep your images optimised and pass `srcSet`, (now [device sizes](https://nextjs.org/docs/api-reference/next/image#device-sizes) in Next.js) & responsive images in modern image formats!

## Conclusion 

Presently, Next.js' Image component only supports displaying a `blurred` placeholder when an image is being loaded, something like what you may have seen on medium.com.

As one solution doesn't fit all use cases, this write-up represents an alternate way to wire up the Image component while still keeping the end user experience in mind.