Tracks whether the image inside a wrapper is loading, loaded or failed, whatever component drew it, so a card or a gallery can swap a broken image for a fallback.
registry/behivetech/hooks/use-image-status at v0.1.0, snapshotted 2026-11-09
use-image-status.ts
1import {2 useEffect,3 useRef,4 useState,5 type ReactEventHandler,6 type RefObject,7} from "react";89/** Where an image is: still loading, shown, or failed. */10export type ImageStatus = "loading" | "loaded" | "error";1112interface ImageStatusResult<T extends HTMLElement> {13 /** Attach to the element that contains the rendered `<img>` */14 ref: RefObject<T | null>;15 /** `loading` until the image settles, then `loaded` or `error` */16 status: ImageStatus;17 /** Alt text of the image that failed, or an empty string, so a fallback can still describe it */18 alt: string;19 /** Spread on the same element: load and error do not bubble, so React's capture phase catches them */20 handlers: {21 onLoadCapture: ReactEventHandler;22 onErrorCapture: ReactEventHandler;23 };24}2526interface ImageResult {27 /** The image the result belongs to; a new key starts again at `loading` */28 key: string;29 status: ImageStatus;30 /** The failed image's alt text, or an empty string */31 alt: string;32}3334/**35 * Tracks whether the `<img>` inside a wrapper has loaded or failed, whatever36 * component drew it (a plain `<img>` or an app's `next/image`). `resetKey`37 * names the image: a new key starts again at `loading`. An image that settled38 * before hydration fires no event, so a new key also reads `complete`.39 *40 * @typeParam T The wrapper element type; defaults to `HTMLDivElement`.41 * @param resetKey Names the image; change it when the image changes.42 * @returns A ref and the capture handlers for the wrapper, the status, and the alt text of a failed image.43 * @example44 * ```tsx45 * const { ref, status, handlers } = useImageStatus(src);46 * <div ref={ref} {...handlers}>47 * {status === "error" ? <Fallback /> : <img src={src} alt="" />}48 * </div>49 * ```50 */51export const useImageStatus = <T extends HTMLElement = HTMLDivElement>(52 resetKey: string,53): ImageStatusResult<T> => {54 const ref = useRef<T | null>(null);55 const [result, setResult] = useState<ImageResult>({56 key: resetKey,57 status: "loading",58 alt: "",59 });60 const isCurrent = result.key === resetKey;61 const status = isCurrent ? result.status : "loading";6263 useEffect(() => {64 const img = ref.current?.querySelector("img");6566 // An image that settled before React attached fires no event later, so a67 // finished image with no pixels (a 404 under SSR) is an error now.68 if (img?.complete) {69 setResult({70 key: resetKey,71 status: img.naturalWidth > 0 ? "loaded" : "error",72 alt: img.alt,73 });74 }75 }, [resetKey]);7677 return {78 ref,79 status,80 alt: status === "error" ? result.alt : "",81 handlers: {82 onLoadCapture: (event) => {83 if (event.target instanceof HTMLImageElement) {84 setResult({ key: resetKey, status: "loaded", alt: "" });85 }86 },87 onErrorCapture: (event) => {88 if (event.target instanceof HTMLImageElement) {89 setResult({ key: resetKey, status: "error", alt: event.target.alt });90 }91 },92 },93 };94};
index.ts
1export { useImageStatus } from "./use-image-status.js";2export type { ImageStatus } from "./use-image-status.js";
use-image-status.example.tsx
1import { useImageStatus } from "./use-image-status.js";23const LOADED_IMAGE =4 "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='100'><rect width='160' height='100' fill='%23c9b6e4'/></svg>";5const BROKEN_IMAGE = "/image-that-does-not-exist.jpg";67const StatusImage = ({ src, alt }: { src: string; alt: string }) => {8 const { ref, status, handlers } = useImageStatus(src);910 return (11 <div ref={ref} {...handlers}>12 <img src={src} alt={alt} width={160} height={100} />13 <p>Status: {status}</p>14 </div>15 );16};1718/** An image that loads reports `loaded`; one that breaks reports `error`. */19export const BasicUseImageStatus = () => (20 <>21 <StatusImage src={LOADED_IMAGE} alt="A lilac swatch" />22 <StatusImage src={BROKEN_IMAGE} alt="A missing picture" />23 </>24);