Onloading Images in React
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.
Source Code
useLoadImage
jamesdrury
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"
/>