Apr 25, 2023April 25th, 2023 · 1 minute read · tweet
useImageIsLoaded
A simple React hook to track the loading state of an 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.
tsexport functionOpen in playgroundfunction 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)useState(false);Returns a stateful value, and a function to update it.
function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): voiduseEffect(() => { if (!Accepts a function that contains imperative, possibly effectful code.
src: string | undefinedsrc) returnconst setIsLoaded: (value: React.SetStateAction<boolean>) => voidsetIsLoaded(false); letlet isMounted: booleanisMounted = true; constconst image: HTMLImageElementimage = newvar window: Window & typeof globalThiswindow.The
windowproperty of a Window object points to the window object itself.var Image: new (width?: number, height?: number) => HTMLImageElementImage(); constconst 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: stringsrc =The
srcproperty of the HTMLImageElement interface specifies the image to display in the element. It reflects the element's src content attribute.src: stringsrc; return () => {let isMounted: booleanisMounted = false; }; }, [src: string | undefinedsrc]); returnconst isLoaded: booleanisLoaded;}
Then you can simply use it inside your component, like so:
tsxconstOpen in playgroundconst imageLoaded: booleanimageLoaded =function useImageIsLoaded(src?: string): booleanuseImageIsLoaded(const imageUrl: stringimageUrl);
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:
![]()
And in a slow network:
![]()
Three important things are going on:
- On both fast and slow networks, the image is loaded all at once, instead of doing this:

-
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.
tsconstOpen in playgroundconst image: HTMLImageElementimage = newvar window: Window & typeof globalThiswindow.The
windowproperty of a Window object points to the window object itself.var Image: new (width?: number, height?: number) => HTMLImageElementImage(); // ...const image: HTMLImageElementimage.HTMLImageElement.src: stringsrc =The
srcproperty of the HTMLImageElement interface specifies the image to display in the element. It reflects the element's src content attribute.src: stringsrc;
Then, when the load or error events are triggered, the state is updated accordingly.
tsOpen in playgroundconst 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);
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.
tsreturnOpen in playgroundconst isLoaded: booleanisLoaded;
Neat, right?