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.

pnpm add @behivetech/hooks.use-image-status

You are viewing Use Image Status v0.1.0, snapshotted 2026-11-09. The latest is v0.1.1. Go to the latest →

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";
8
9/** Where an image is: still loading, shown, or failed. */
10export type ImageStatus = "loading" | "loaded" | "error";
11
12interface 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}
25
26interface 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}
33
34/**
35 * Tracks whether the `<img>` inside a wrapper has loaded or failed, whatever
36 * 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 settled
38 * 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 * @example
44 * ```tsx
45 * 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";
62
63 useEffect(() => {
64 const img = ref.current?.querySelector("img");
65
66 // An image that settled before React attached fires no event later, so a
67 // 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]);
76
77 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";
2
3const 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";
6
7const StatusImage = ({ src, alt }: { src: string; alt: string }) => {
8 const { ref, status, handlers } = useImageStatus(src);
9
10 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};
17
18/** 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);