fix(use-image): load images after props change (#4523)

* fix(use-image): load image after props change

* chore(changeset): add changeset

* refactor(use-image): remove unused props

* feat(use-image): add test case

* fix(use-image): apply useCallback to load & remove status check

* chore(changeset): update package name
This commit is contained in:
աӄա 2025-01-30 20:44:38 +08:00 committed by GitHub
parent 8452603b5b
commit f9c2be4509
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 42 additions and 29 deletions

View File

@ -0,0 +1,5 @@
---
"@heroui/use-image": patch
---
fix loading image after props changes (#4518)

View File

@ -27,6 +27,19 @@ describe("use-image hook", () => {
await waitFor(() => expect(result.current).toBe("loaded"));
});
it("can handle changing image", async () => {
const {result, rerender} = renderHook(() => useImage({src: undefined}));
expect(result.current).toEqual("pending");
setTimeout(() => {
rerender({src: "/test.png"});
}, 3000);
mockImage.simulate("loaded");
await waitFor(() => expect(result.current).toBe("loaded"));
});
it("can handle error image", async () => {
mockImage.simulate("error");
const {result} = renderHook(() => useImage({src: "/test.png"}));

View File

@ -4,7 +4,7 @@
import type {ImgHTMLAttributes, SyntheticEvent} from "react";
import {useRef, useState, useEffect, MutableRefObject} from "react";
import {useRef, useState, useEffect, useCallback} from "react";
import {useIsHydrated} from "@heroui/react-utils";
import {useSafeLayoutEffect} from "@heroui/use-safe-layout-effect";
@ -66,7 +66,7 @@ type ImageEvent = SyntheticEvent<HTMLImageElement, Event>;
*/
export function useImage(props: UseImageProps = {}) {
const {onLoad, onError, ignoreFallback} = props;
const {onLoad, onError, ignoreFallback, src, crossOrigin, srcSet, sizes, loading} = props;
const isHydrated = useIsHydrated();
@ -96,11 +96,31 @@ export function useImage(props: UseImageProps = {}) {
}
};
const load = useCallback((): Status => {
if (!src) return "pending";
if (ignoreFallback) return "loaded";
const img = new Image();
img.src = src;
if (crossOrigin) img.crossOrigin = crossOrigin;
if (srcSet) img.srcset = srcSet;
if (sizes) img.sizes = sizes;
if (loading) img.loading = loading;
imageRef.current = img;
if (img.complete && img.naturalWidth) {
return "loaded";
}
return "loading";
}, [src, crossOrigin, srcSet, sizes, onLoad, onError, loading]);
useSafeLayoutEffect(() => {
if (isHydrated) {
setStatus(setImageAndGetInitialStatus(props, imageRef));
setStatus(load());
}
}, [isHydrated]);
}, [isHydrated, load]);
/**
* If user opts out of the fallback/placeholder
@ -109,31 +129,6 @@ export function useImage(props: UseImageProps = {}) {
return ignoreFallback ? "loaded" : status;
}
function setImageAndGetInitialStatus(
props: UseImageProps,
imageRef: MutableRefObject<HTMLImageElement | null | undefined>,
): Status {
const {loading, src, srcSet, crossOrigin, sizes, ignoreFallback} = props;
if (!src) return "pending";
if (ignoreFallback) return "loaded";
const img = new Image();
img.src = src;
if (crossOrigin) img.crossOrigin = crossOrigin;
if (srcSet) img.srcset = srcSet;
if (sizes) img.sizes = sizes;
if (loading) img.loading = loading;
imageRef.current = img;
if (img.complete && img.naturalWidth) {
return "loaded";
}
return "loading";
}
export const shouldShowFallbackImage = (status: Status, fallbackStrategy: FallbackStrategy) =>
(status !== "loaded" && fallbackStrategy === "beforeLoadOrError") ||
(status === "failed" && fallbackStrategy === "onError");