30% offEnding soon
useImageOnLoadLoading saved progress…

useImageOnLoad

useImageOnLoad tracks one image's loading lifecycle — loading, then loaded or error — and hands it back as React state you can render a spinner, the image, or a fallback against. You attach the hook's ref to your own <img> element and read its status. The catch is the browser cache: an image you have already seen can finish loading before your listener attaches, so relying on the load event alone leaves the status stuck at loading even though the picture is right there on screen.

Implement useImageOnLoad(src). It returns { ref, status }: spread ref on an <img> and read status. When src changes, it resets to loading and tracks the new image.

Signature

function useImageOnLoad(src) {
  // returns { ref, status }
  //   ref:    attach to your image  ->  <img ref={ref} src={src} />
  //   status: 'loading' | 'loaded' | 'error'
}

Examples

const { ref, status } = useImageOnLoad(src);
return (
  <div>
    {status === 'loading' && <Spinner />}
    <img ref={ref} src={src} alt="" hidden={status !== 'loaded'} />
    {status === 'error' && <BrokenIcon />}
  </div>
);
// status is 'loading', then 'loaded' once the image finishes.
// Second visit: the image is served from cache. It is already `complete`
// before the effect runs, so no `load` event fires. status must still become
// 'loaded' from a synchronous check — not stay stuck at 'loading'.

Notes

  • Three states. Start at loading. A load event means loaded; an error event means error.
  • Cached images. An image already in cache can be finished before your listener attaches. Check img.complete synchronously so you do not miss it.
  • Broken images. A complete image with naturalWidth === 0 finished but has no pixels — that is a failed load, so report error.
  • New src. When src changes, reset to loading, track the new image, and clean up the old listeners.
  • The ref is yours. The hook returns a ref to spread on an <img> you render; it does not render one for you. You still set src, alt, and sizing.
  • Out of scope. Blur-up placeholders, off-thread decode(), and srcset are extensions — see the solution's "Going further".