Onloading Images in React

James Drury
James Drury / Aug 15, 2024
4 min read ・ 201 views

Typically, when working with images, it is best to display the rest of your UI while an image is still loading. However, if an image takes some time to load, this can make for a poor user experience.

In this note, we will create a hook that tracks the loading status of an image, in order to adapt the rest of the UI while that image is still loading.

Render vs Load

With modern frameworks like `NextJs`, rendering and pre-rendering have become popular concepts in the React community. NextJs offers several rendering capabilities to optimize applications.

However, it is important to note that just because an image is `rendered`, does not mean it is `loaded`. When an image is first rendered to the DOM, the data for that image may still be downloading, and not yet visible to users.

In order to manage UI during this time, we have to be able to detect if an image is fully loaded. We can do this in two steps:

`onload`

One, is by using the JavaScript onload event. `onload` fires when a resource is finished loading. This could be any resource in JavaScript (e.g. entire page, an image, a stylesheet…)

window.onload = () => console.log("Loaded")

useEffect

Second, we can handle this onload event by using `useEffect`. Because useEffect is executed only after a component is rendered, it is the perfect place to handle onload events. In other words, eecause elements that are called inside useEffect have already been rendered to the DOM, we can easily detect if those elements have also been loaded using the onload event.

Let’s see how this works in our custom hook:

useLoadImage()

export default function useLoadImage(src: string) { const [loaded, loadedSet] = React.useState(false); const loadImage = (srcset: string) => { const img = new Image(); img.src = srcset img.onload = () => loadedSet(true) }; React.useEffect(() => loadImage(src), [src]); return loaded; }

If you notice in the code above, once the onload event detects the image resource, the loaded state is set to true. Our custom hook then returns the updated state, so that it can be used when we call this hook from our UI component:

function App() { const loaded = useLoadImage(src) const spinner = !loaded && <p>Loading...</p>; const style = { display: loaded ? "block" : "none" }; return ( <main> <h1 style={style}>Loaded image</h1> <img src={src} style={style} alt="preloaded" /> {spinner} </main> ); }

However, there is one caveat. If the image happens to throw an error, the onload event will not fire, even though, technically, the image would have finished loading. Consequently, the loaded state will never be set to true and the UI will never update. To prevent this, just like JavaScript has an onload event when a resource loads, it also has an `onerror` event for when a resource fails:

const loadImage = (srcset: string) => { const img = new Image(); img.src = srcset img.onload = () => loadedSet(true) img.onerror = () => loadedSet(true) //add onerror };

Refactor Hook

Although it is nice to be able to handle both, a resource that loads and a resource that fails, in this case it is not necessary. Here, we simply need to know whether an image is finished loading, regardless of whether it succeeds or fails. With this in mind, we can swap out onload and onerror with the `complete` property instead:

const loadImage = (srcset: string) => { const img = new Image(); img.src = srcset; loadedSet(img.complete); //swap in complete };

The complete property is an image property that is set to true when any of the following events occur:

The src attribute is not specified

The image resource is fully loaded

The image fails due to an error

In other words, complete is set to true when the image stops loading for any reason. This is the better option for our hook.

The `onload` Attribute

Finally, for a more concise solution, instead of using onload inside of a hook, we can use the `onload attribute` inside the html `<img />` tag. Just as before, the onload attribute, calls a function when the image is loaded. In the example below, we will simply update a piece of state using this attribute:

function App() { const [loaded, loadedSet] = React.useState(false); const spinner = !loaded && <p>Loading...</p>; const style = { display: loaded ? "block" : "none" }; return ( <main> <h1 style={style}>Loaded image</h1> <img src={src} style={style} onLoad={() => loadedSet(true)} alt="preloaded" /> {spinner} </main> ); }

The <img /> tag also comes with the `onerror attribute`, in the case the image fails to load:

<img src={src} style={style} onLoad={() => loadedSet(true)} onError={() => loadedSet(true)} alt="preloaded" />

Resources

Subscribe to my newsletter

Get updates on my new notes