How to load images with React

Karl
4 min readMay 15, 2023

--

During my developer career, I noticed that there is always this set of the same problems that I have to solve every time I start working on some new project. One of such problem is “<Image /> loading”. So I created this tiny library called loadable-image, that takes care of this.

Every time I speak about the image loading process I think about 4 main steps that have to be done.

1-st. Lazy load — We want to lazy load images (lazy loading is a technique that allows you to load only images when they are visible in the viewport this way we can save users traffic and also speed up the initial page load time).

2-nd. Loading state We want to display some placeholder during the loading state to avoid layout shifting after image load.

3-rd. Error state — We want to keep track of the loading status to properly handle and display the error state.

Finally. Transition animation — We want to animate the transition between loading/error/loaded states. For example, we could have a blurred (1px x 1px) image as a loader which we could animate from blur to loaded state.

Demo

Check out this demo. (Notice how we animate between the loader & loaded image)

Implementation

I will omit some details in the code snippets only keeping the crucial parts to make it easier to understand the main idea, if you are interested in the final implementation feel free to check out the source code of loadable-image

Let's start. In order to implement lazy loading we need to download (render) only images that are visible in the viewport. To do so we can use Intersection Observer API. In this example, I’m using react-intersection-observer library that can tell us when an element enters or leaves the viewport.

import { InView } from 'react-intersection-observer'

export const AsyncImage = (imageProps) => {
return (
<InView triggerOnce>
{({ ref, inView }) => (
<div ref={ref}>
<Image inView={inView} {...imageProps} />
</div>
)}
</InView>
)
}

Since we don’t want to re-download our image every time it leaves and enters the viewport again we pass triggerOnce prop to the <InView /> component.

Now let’s look at the <Image /> component.

  • We only want to render (download) the image if inView is true
  • We want to track the loading status to display loading/error states.
export const Image = ({ inView, ...imageProps }) => {
const [status, setStatus] = useState('loading')

return (
<>
{ status === 'loading' && (<div >loading...</div>)}
{ inView && (
<img
{...imageProps}
onLoad={() => setStatus('loaded')}
onError={() => setStatus('failed')}
/>
)}
{ status === 'failed' && (<div>error</div>)}
</>
)
}

As you can see the logic here is really simple. By default, we display the loading state and we render <img /> only when the element enters the viewport (inVew will be true). Finally, we added onLoad & onError callbacks, to update our status state.

The last step is to handle transition animation between different states. I will use the <Fade /> component from transitions-kit (this library is just a set of predefined transition components built on top of react-transition-gorup) I am planning to write a separate post about this lib later (react-transition-group is maintained by react team itself and even lives in React repository). This is how our <Image /> component will look like using <Fade /> transition.

import { Fade } from 'transitions-kit'

export const Image = ({ src }) => {
const [status, setStatus] = useState('loading')

return (
<>
<Fade appear={false} in={status === 'loading'} unmountOnExit>
<div >loading...</div>
</Fade>

{inView && (
<Fade in={status === 'loaded'}>
<img
{...imageProps}
onLoad={() => setStatus('loaded')}
onError={() => setStatus('failed')}
/>
</Fade>
)}

<Fade in={status === 'failed'} mountOnEnter unmountOnExit>
<div>error</div>
</Fade>
</>
)
}

Notice here we didn’t specify mountOnEnter for our <img />, It will render the image as soon as it will enter the viewport (inView is true) but it will be hidden by <Fade/> component until its download status wont be “loaded”

Congratulations! Now we have a way to lazy load images and animate between loader/error states based on the loading status.

Loadable-image

As I said it's a simplified version of the code. But If you want to try this on your own projects feel free to use loadable-image library. The API is really simple it supports all HTML image attributes like (srcSet, refererPolicy, etc).

  • The only requirement is to specify width & height (you will need to pass them in style prop)

Additionally, you can provide these custom props:

  • loader — React element to display a loading state.
  • error — React element to display an error state.
  • rootMargin — Margin around the root. Can have values similar to the CSS margin property, e.g. "10px 20px 30px 40px" (top, right, bottom, left). Specifies the area around the image intersection which will trigger an image download.
  • Transition — Custom Transition component. Check out transitions-kit’s predefined components
import { AsyncImage } from 'loadable-image'


<AsyncImage
src='https://picsum.photos/1900'
style={{ width: 150, height: 150}}
/>

// or use custom loader /error components
<AsyncImage
src='https://picsum.photos/1900'
style={{ width: 150, height: 150 }}
loader={<div style={{ background: '#888' }} />}
error={<div style={{ background: '#222' }} />}
/>

// here is blur example
<AsyncImage
src='largeImage.jpg'
style={{ width: 150, height: 150 }}
loader={<img src='smallImage.jpg' style={{filter: 'blur(25px)'}} />}
error={<div style={{ background: '#222' }} />}
/>

...
// Custom Transition
import { Blur } from 'transitions-kit'

<AsyncImage
src='https://picsum.photos/1900'
style={{ width: 150, height: 150}}
Transition={Blur}
/>

Thank you for your time, I hope it was helpful. Feel free to reach me out on github, twitter or leave some comments below.

--

--

No responses yet