mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
fix(use-image): image ReferenceError in SSR (#4122)
* fix(use-image): image ReferenceError in SSR * fix(use-image): sync with beta * fix(use-image): sync with beta * chore(use-image): remove unnecessary comments
This commit is contained in:
parent
c6326d169d
commit
ce1546c983
5
.changeset/wild-jobs-explain.md
Normal file
5
.changeset/wild-jobs-explain.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"@nextui-org/use-image": patch
|
||||
---
|
||||
|
||||
fix Image ReferenceError in SSR
|
||||
@ -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"));
|
||||
});
|
||||
});
|
||||
|
||||
@ -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<HTMLImageElement>;
|
||||
@ -46,7 +47,6 @@ type Status = "loading" | "failed" | "pending" | "loaded";
|
||||
export type FallbackStrategy = "onError" | "beforeLoadOrError";
|
||||
|
||||
type ImageEvent = SyntheticEvent<HTMLImageElement, Event>;
|
||||
|
||||
/**
|
||||
* 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<HTMLImageElement, Event>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
export function useImage(props: UseImageProps = {}) {
|
||||
const {loading, src, srcSet, onLoad, onError, crossOrigin, sizes, ignoreFallback} = props;
|
||||
|
||||
const imageRef = useRef<HTMLImageElement | null>();
|
||||
const firstMount = useRef<boolean>(true);
|
||||
const [status, setStatus] = useState<Status>(() => setImageAndGetInitialStatus(props, imageRef));
|
||||
|
||||
useSafeLayoutEffect(() => {
|
||||
if (firstMount.current) {
|
||||
firstMount.current = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(setImageAndGetInitialStatus(props, imageRef));
|
||||
|
||||
return () => {
|
||||
flush();
|
||||
};
|
||||
}, [src, crossOrigin, srcSet, sizes, loading]);
|
||||
const [status, setStatus] = useState<Status>("pending");
|
||||
|
||||
useEffect(() => {
|
||||
if (!imageRef.current) return;
|
||||
imageRef.current.onload = (event) => {
|
||||
setStatus(src ? "loading" : "pending");
|
||||
}, [src]);
|
||||
|
||||
const imageRef = useRef<HTMLImageElement | null>();
|
||||
|
||||
const load = useCallback(() => {
|
||||
if (!src) return;
|
||||
|
||||
flush();
|
||||
|
||||
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;
|
||||
|
||||
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<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");
|
||||
|
||||
export type UseImageReturn = ReturnType<typeof useImage>;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user