diff --git a/.changeset/light-peaches-reflect.md b/.changeset/light-peaches-reflect.md new file mode 100644 index 0000000000..d708781832 --- /dev/null +++ b/.changeset/light-peaches-reflect.md @@ -0,0 +1,5 @@ +--- +"@heroui/use-image": patch +--- + +fix loading image after props changes (#4518) diff --git a/packages/hooks/use-image/__tests__/use-image.test.tsx b/packages/hooks/use-image/__tests__/use-image.test.tsx index 043fbd2507..f53db20eb9 100644 --- a/packages/hooks/use-image/__tests__/use-image.test.tsx +++ b/packages/hooks/use-image/__tests__/use-image.test.tsx @@ -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"})); diff --git a/packages/hooks/use-image/src/index.ts b/packages/hooks/use-image/src/index.ts index dd47fd99a7..3960f9af94 100644 --- a/packages/hooks/use-image/src/index.ts +++ b/packages/hooks/use-image/src/index.ts @@ -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 "@nextui-org/react-utils"; import {useSafeLayoutEffect} from "@nextui-org/use-safe-layout-effect"; @@ -66,7 +66,7 @@ type ImageEvent = SyntheticEvent; */ 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, -): 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");