From 52f6274e9baecc928ee7cbb6a8110b72931061b8 Mon Sep 17 00:00:00 2001 From: Junior Garcia Date: Tue, 5 Nov 2024 17:15:27 -0300 Subject: [PATCH] fix: image load on next.js (#3998) --- .changeset/gentle-needles-rescue.md | 5 + .../use-image/__tests__/use-image.test.tsx | 7 - packages/hooks/use-image/src/index.ts | 241 ++++++++++++++---- 3 files changed, 195 insertions(+), 58 deletions(-) create mode 100644 .changeset/gentle-needles-rescue.md diff --git a/.changeset/gentle-needles-rescue.md b/.changeset/gentle-needles-rescue.md new file mode 100644 index 0000000000..7c51ef52b4 --- /dev/null +++ b/.changeset/gentle-needles-rescue.md @@ -0,0 +1,5 @@ +--- +"@nextui-org/use-image": patch +--- + +use-image hook logic restore to the previous one to avoid nextjs hydration issues diff --git a/packages/hooks/use-image/__tests__/use-image.test.tsx b/packages/hooks/use-image/__tests__/use-image.test.tsx index c2e5860c64..f69de371f6 100644 --- a/packages/hooks/use-image/__tests__/use-image.test.tsx +++ b/packages/hooks/use-image/__tests__/use-image.test.tsx @@ -34,11 +34,4 @@ describe("use-image hook", () => { expect(result.current).toEqual("loading"); await waitFor(() => expect(result.current).toBe("failed")); }); - - it("can handle cached image", async () => { - mockImage.simulate("loaded"); - const {result} = renderHook(() => useImage({src: "/test.png"})); - - expect(result.current).toEqual("loaded"); - }); }); diff --git a/packages/hooks/use-image/src/index.ts b/packages/hooks/use-image/src/index.ts index 2c1aab7f4d..a935d00ab3 100644 --- a/packages/hooks/use-image/src/index.ts +++ b/packages/hooks/use-image/src/index.ts @@ -1,9 +1,160 @@ +// /** +// * 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 {useSafeLayoutEffect} from "@nextui-org/use-safe-layout-effect"; + +// type NativeImageProps = ImgHTMLAttributes; + +// export interface UseImageProps { +// /** +// * The image `src` attribute +// */ +// src?: string; +// /** +// * The image `srcset` attribute +// */ +// srcSet?: string; +// /** +// * The image `sizes` attribute +// */ +// sizes?: string; +// /** +// * A callback for when the image `src` has been loaded +// */ +// onLoad?: NativeImageProps["onLoad"]; +// /** +// * A callback for when there was an error loading the image `src` +// */ +// onError?: NativeImageProps["onError"]; +// /** +// * If `true`, opt out of the `fallbackSrc` logic and use as `img` +// */ +// ignoreFallback?: boolean; +// /** +// * The key used to set the crossOrigin on the HTMLImageElement into which the image will be loaded. +// * This tells the browser to request cross-origin access when trying to download the image data. +// */ +// crossOrigin?: NativeImageProps["crossOrigin"]; +// loading?: NativeImageProps["loading"]; +// } + +// 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 +// * fallback if it is still `pending` +// * +// * @returns the status of the image loading progress +// * +// * @example +// * +// * ```jsx +// * function App(){ +// * const status = useImage({ src: "image.png" }) +// * return status === "loaded" ? : +// * } +// * ``` +// */ +// export function useImage(props: UseImageProps = {}) { +// const {loading, src, srcSet, onLoad, onError, crossOrigin, sizes, ignoreFallback} = props; + +// const imageRef = useRef(); +// const firstMount = useRef(true); +// const [status, setStatus] = useState(() => setImageAndGetInitialStatus(props, imageRef)); + +// useSafeLayoutEffect(() => { +// if (firstMount.current) { +// firstMount.current = false; + +// return; +// } + +// setStatus(setImageAndGetInitialStatus(props, imageRef)); + +// return () => { +// flush(); +// }; +// }, [src, crossOrigin, srcSet, sizes, loading]); + +// useEffect(() => { +// if (!imageRef.current) return; +// imageRef.current.onload = (event) => { +// flush(); +// setStatus("loaded"); +// onLoad?.(event as unknown as ImageEvent); +// }; +// imageRef.current.onerror = (error) => { +// flush(); +// setStatus("failed"); +// onError?.(error as any); +// }; +// }, [imageRef.current]); + +// const flush = () => { +// if (imageRef.current) { +// imageRef.current.onload = null; +// imageRef.current.onerror = null; +// imageRef.current = null; +// } +// }; + +// /** +// * 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"; + +// try { +// 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"; +// } catch (error) { +// return "loading"; +// } +// } + +// export const shouldShowFallbackImage = (status: Status, fallbackStrategy: FallbackStrategy) => +// (status !== "loaded" && fallbackStrategy === "beforeLoadOrError") || +// (status === "failed" && fallbackStrategy === "onError"); + +// export type UseImageReturn = ReturnType; + /** * 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 +197,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 +213,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 +260,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 = typeof window !== "undefined" ? new Image() : document.createElement("img"); - - 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;