dio.la

Dani Guardiola’s blog

Apr 25, 2023April 25th, 2023 · 1 minute read · tweet

useImageIsLoaded

A simple React hook to track the loading state of an image.

This article's main image

A very common pattern for components like avatars is to detect whether an image has been loaded to adapt the UI accordingly. It allows the component to react gracefully and prevent jarring changes or flashes of content.

Here's a simple React hook that tracks the loading state of an image. It was built by the awesome Radix UI team as useImageLoadingStatus, I only made minor changes so all credit goes to them.

ts
export function function useImageIsLoaded(src?: string): booleanuseImageIsLoaded(src: string | undefinedsrc?: string) { const [const isLoaded: booleanisLoaded, const setIsLoaded: React.Dispatch<React.SetStateAction<boolean>>setIsLoaded] = useState<boolean>(initialState: boolean | (() => boolean)): [boolean, React.Dispatch<React.SetStateAction<boolean>>] (+1 overload)

Returns a stateful value, and a function to update it.

useState
(false);
function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): void

Accepts a function that contains imperative, possibly effectful code.

@parameffect Imperative function that can return a cleanup function@paramdeps If present, effect will only activate if the values in the list change.@version16.8.0@seehttps://react.dev/reference/react/useEffect
useEffect
(() => {
if (!src: string | undefinedsrc) return const setIsLoaded: (value: React.SetStateAction<boolean>) => voidsetIsLoaded(false); let let isMounted: booleanisMounted = true; const const image: HTMLImageElementimage = new var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
.var Image: new (width?: number, height?: number) => HTMLImageElementImage();
const const createStatusHandler: (status: boolean) => () => voidcreateStatusHandler = (status: booleanstatus: boolean) => () => { if (let isMounted: booleanisMounted) const setIsLoaded: (value: React.SetStateAction<boolean>) => voidsetIsLoaded(status: booleanstatus); }; const setIsLoaded: (value: React.SetStateAction<boolean>) => voidsetIsLoaded(false); const image: HTMLImageElementimage.GlobalEventHandlers.onload: ((this: GlobalEventHandlers, ev: Event) => any) | nullonload = const createStatusHandler: (status: boolean) => () => voidcreateStatusHandler(true); const image: HTMLImageElementimage.GlobalEventHandlers.onerror: OnErrorEventHandleronerror = const createStatusHandler: (status: boolean) => () => voidcreateStatusHandler(false); const image: HTMLImageElementimage.HTMLImageElement.src: string

The src property of the HTMLImageElement interface specifies the image to display in the element. It reflects the element's src content attribute.

MDN Reference

src
= src: stringsrc;
return () => { let isMounted: booleanisMounted = false; }; }, [src: string | undefinedsrc]); return const isLoaded: booleanisLoaded;}
Open in playground

Then you can simply use it inside your component, like so:

tsx
const const imageLoaded: booleanimageLoaded = function useImageIsLoaded(src?: string): booleanuseImageIsLoaded(const imageUrl: stringimageUrl);Open in playground

In action

I used this hook at Guide to ensure that the loading experience of the Avatar component was smooth on fast and slow networks, even in the event of failure.

Here's what it looks like if you load it on a fast network:

A fast network loading the avatar

And in a slow network:

A slow network loading the avatar

Three important things are going on:

  • On both fast and slow networks, the image is loaded all at once, instead of doing this:

A meme gif of an image falsely loading slowly top to bottom

  • Since the image will always load at least a bit after the component itself is rendered, even on fast networks, there's a fade-in effect to prevent a sudden flash.

  • Finally, since the image could take a while (or even fail) to load in slow networks, the avatar will fall back to displaying the initials. It doesn't happen instantly though: there's a small delay (one second in this case) so that the user doesn't see the initials flash if the image loads quickly.

All in all, this technique makes the experience for the user much more pleasant and significantly less jarring.

Deconstructing the hook

This hook is pretty clever and surprisingly simple.

It takes the target image URL (src) and creates an Image instance that uses it as source. This is functionally equivalent to creating an img element (e.g. document.createElement('img')) without adding it to the DOM.

ts
const const image: HTMLImageElementimage = new var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
.var Image: new (width?: number, height?: number) => HTMLImageElementImage();
// ... const image: HTMLImageElementimage.HTMLImageElement.src: string

The src property of the HTMLImageElement interface specifies the image to display in the element. It reflects the element's src content attribute.

MDN Reference

src
= src: stringsrc;
Open in playground

Then, when the load or error events are triggered, the state is updated accordingly.

ts
const image: HTMLImageElementimage.GlobalEventHandlers.onload: ((this: GlobalEventHandlers, ev: Event) => any) | nullonload = const createStatusHandler: (status: boolean) => () => voidcreateStatusHandler(true); const image: HTMLImageElementimage.GlobalEventHandlers.onerror: OnErrorEventHandleronerror = const createStatusHandler: (status: boolean) => () => voidcreateStatusHandler(false);Open in playground

Finally, the state is returned. If the image is loading or failed to load, the state will be false. If it loaded successfully, the state will be true.

ts
return const isLoaded: booleanisLoaded;Open in playground

Neat, right?

Keep up with my stuff. Zero spam.