diff --git a/.changeset/wild-jobs-explain.md b/.changeset/wild-jobs-explain.md new file mode 100644 index 0000000000..4ae1929636 --- /dev/null +++ b/.changeset/wild-jobs-explain.md @@ -0,0 +1,5 @@ +--- +"@nextui-org/use-image": patch +--- + +fix Image ReferenceError in SSR \ No newline at end of file diff --git a/packages/hooks/use-image/__tests__/use-image.test.tsx b/packages/hooks/use-image/__tests__/use-image.test.tsx index 7f76d4a7cd..f69de371f6 100644 --- a/packages/hooks/use-image/__tests__/use-image.test.tsx +++ b/packages/hooks/use-image/__tests__/use-image.test.tsx @@ -1,4 +1,4 @@ -import {renderHook} from "@testing-library/react-hooks"; +import {renderHook, waitFor} from "@testing-library/react"; import {mocks} from "@nextui-org/test-utils"; import {useImage} from "../src"; @@ -14,31 +14,24 @@ describe("use-image hook", () => { }); it("can handle missing src", () => { - const rendered = renderHook(() => useImage({})); + const {result} = renderHook(() => useImage({})); - expect(rendered.result.current).toEqual("pending"); + expect(result.current).toEqual("pending"); }); it("can handle loading image", async () => { - const rendered = renderHook(() => useImage({src: "/test.png"})); + const {result} = renderHook(() => useImage({src: "/test.png"})); - expect(rendered.result.current).toEqual("loading"); + expect(result.current).toEqual("loading"); mockImage.simulate("loaded"); - await rendered.waitForValueToChange(() => rendered.result.current === "loaded"); + await waitFor(() => expect(result.current).toBe("loaded")); }); it("can handle error image", async () => { mockImage.simulate("error"); - const rendered = renderHook(() => useImage({src: "/test.png"})); + const {result} = renderHook(() => useImage({src: "/test.png"})); - expect(rendered.result.current).toEqual("loading"); - await rendered.waitForValueToChange(() => rendered.result.current === "failed"); - }); - - it("can handle cached image", async () => { - mockImage.simulate("loaded"); - const rendered = renderHook(() => useImage({src: "/test.png"})); - - expect(rendered.result.current).toEqual("loaded"); + expect(result.current).toEqual("loading"); + await waitFor(() => expect(result.current).toBe("failed")); }); }); diff --git a/packages/hooks/use-image/src/index.ts b/packages/hooks/use-image/src/index.ts index 531b2b98ee..d0a75b67cb 100644 --- a/packages/hooks/use-image/src/index.ts +++ b/packages/hooks/use-image/src/index.ts @@ -1,9 +1,10 @@ /** * Part of this code is taken from @chakra-ui/react package ❤️ */ -import type {ImgHTMLAttributes, MutableRefObject, SyntheticEvent} from "react"; -import {useEffect, useRef, useState} from "react"; +import type {ImgHTMLAttributes, SyntheticEvent} from "react"; + +import {useCallback, useEffect, useRef, useState} from "react"; import {useSafeLayoutEffect} from "@nextui-org/use-safe-layout-effect"; type NativeImageProps = ImgHTMLAttributes; @@ -46,7 +47,6 @@ type Status = "loading" | "failed" | "pending" | "loaded"; export type FallbackStrategy = "onError" | "beforeLoadOrError"; type ImageEvent = SyntheticEvent; - /** * React hook that loads an image in the browser, * and lets us know the `status` so we can show image @@ -63,40 +63,44 @@ type ImageEvent = SyntheticEvent; * } * ``` */ + export function useImage(props: UseImageProps = {}) { const {loading, src, srcSet, onLoad, onError, crossOrigin, sizes, ignoreFallback} = props; + const [status, setStatus] = useState("pending"); + + useEffect(() => { + setStatus(src ? "loading" : "pending"); + }, [src]); + const imageRef = useRef(); - const firstMount = useRef(true); - const [status, setStatus] = useState(() => setImageAndGetInitialStatus(props, imageRef)); - useSafeLayoutEffect(() => { - if (firstMount.current) { - firstMount.current = false; + const load = useCallback(() => { + if (!src) return; - return; - } + flush(); - setStatus(setImageAndGetInitialStatus(props, imageRef)); + const img = new Image(); - return () => { - flush(); - }; - }, [src, crossOrigin, srcSet, sizes, loading]); + img.src = src; + if (crossOrigin) img.crossOrigin = crossOrigin; + if (srcSet) img.srcset = srcSet; + if (sizes) img.sizes = sizes; + if (loading) img.loading = loading; - useEffect(() => { - if (!imageRef.current) return; - imageRef.current.onload = (event) => { + img.onload = (event) => { flush(); setStatus("loaded"); onLoad?.(event as unknown as ImageEvent); }; - imageRef.current.onerror = (error) => { + img.onerror = (error) => { flush(); setStatus("failed"); onError?.(error as any); }; - }, [imageRef.current]); + + imageRef.current = img; + }, [src, crossOrigin, srcSet, sizes, onLoad, onError, loading]); const flush = () => { if (imageRef.current) { @@ -106,40 +110,25 @@ export function useImage(props: UseImageProps = {}) { } }; + useSafeLayoutEffect(() => { + /** + * If user opts out of the fallback/placeholder + * logic, let's bail out. + */ + if (ignoreFallback) return undefined; + + if (status === "loading") { + load(); + } + + return () => { + flush(); + }; + }, [status, load, ignoreFallback]); + /** * If user opts out of the fallback/placeholder * logic, let's just return 'loaded' */ 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"); - -export type UseImageReturn = ReturnType;