Skip to content

Commit e45526f

Browse files
committed
Merge branch 'canary' into feat/eng-1582
2 parents 66ad869 + ce1546c commit e45526f

File tree

3 files changed

+54
-67
lines changed

3 files changed

+54
-67
lines changed

.changeset/wild-jobs-explain.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nextui-org/use-image": patch
3+
---
4+
5+
fix Image ReferenceError in SSR
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {renderHook} from "@testing-library/react-hooks";
1+
import {renderHook, waitFor} from "@testing-library/react";
22
import {mocks} from "@nextui-org/test-utils";
33

44
import {useImage} from "../src";
@@ -14,31 +14,24 @@ describe("use-image hook", () => {
1414
});
1515

1616
it("can handle missing src", () => {
17-
const rendered = renderHook(() => useImage({}));
17+
const {result} = renderHook(() => useImage({}));
1818

19-
expect(rendered.result.current).toEqual("pending");
19+
expect(result.current).toEqual("pending");
2020
});
2121

2222
it("can handle loading image", async () => {
23-
const rendered = renderHook(() => useImage({src: "/test.png"}));
23+
const {result} = renderHook(() => useImage({src: "/test.png"}));
2424

25-
expect(rendered.result.current).toEqual("loading");
25+
expect(result.current).toEqual("loading");
2626
mockImage.simulate("loaded");
27-
await rendered.waitForValueToChange(() => rendered.result.current === "loaded");
27+
await waitFor(() => expect(result.current).toBe("loaded"));
2828
});
2929

3030
it("can handle error image", async () => {
3131
mockImage.simulate("error");
32-
const rendered = renderHook(() => useImage({src: "/test.png"}));
32+
const {result} = renderHook(() => useImage({src: "/test.png"}));
3333

34-
expect(rendered.result.current).toEqual("loading");
35-
await rendered.waitForValueToChange(() => rendered.result.current === "failed");
36-
});
37-
38-
it("can handle cached image", async () => {
39-
mockImage.simulate("loaded");
40-
const rendered = renderHook(() => useImage({src: "/test.png"}));
41-
42-
expect(rendered.result.current).toEqual("loaded");
34+
expect(result.current).toEqual("loading");
35+
await waitFor(() => expect(result.current).toBe("failed"));
4336
});
4437
});

packages/hooks/use-image/src/index.ts

+40-51
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/**
22
* Part of this code is taken from @chakra-ui/react package ❤️
33
*/
4-
import type {ImgHTMLAttributes, MutableRefObject, SyntheticEvent} from "react";
54

6-
import {useEffect, useRef, useState} from "react";
5+
import type {ImgHTMLAttributes, SyntheticEvent} from "react";
6+
7+
import {useCallback, useEffect, useRef, useState} from "react";
78
import {useSafeLayoutEffect} from "@nextui-org/use-safe-layout-effect";
89

910
type NativeImageProps = ImgHTMLAttributes<HTMLImageElement>;
@@ -46,7 +47,6 @@ type Status = "loading" | "failed" | "pending" | "loaded";
4647
export type FallbackStrategy = "onError" | "beforeLoadOrError";
4748

4849
type ImageEvent = SyntheticEvent<HTMLImageElement, Event>;
49-
5050
/**
5151
* React hook that loads an image in the browser,
5252
* and lets us know the `status` so we can show image
@@ -63,40 +63,44 @@ type ImageEvent = SyntheticEvent<HTMLImageElement, Event>;
6363
* }
6464
* ```
6565
*/
66+
6667
export function useImage(props: UseImageProps = {}) {
6768
const {loading, src, srcSet, onLoad, onError, crossOrigin, sizes, ignoreFallback} = props;
6869

70+
const [status, setStatus] = useState<Status>("pending");
71+
72+
useEffect(() => {
73+
setStatus(src ? "loading" : "pending");
74+
}, [src]);
75+
6976
const imageRef = useRef<HTMLImageElement | null>();
70-
const firstMount = useRef<boolean>(true);
71-
const [status, setStatus] = useState<Status>(() => setImageAndGetInitialStatus(props, imageRef));
7277

73-
useSafeLayoutEffect(() => {
74-
if (firstMount.current) {
75-
firstMount.current = false;
78+
const load = useCallback(() => {
79+
if (!src) return;
7680

77-
return;
78-
}
81+
flush();
7982

80-
setStatus(setImageAndGetInitialStatus(props, imageRef));
83+
const img = new Image();
8184

82-
return () => {
83-
flush();
84-
};
85-
}, [src, crossOrigin, srcSet, sizes, loading]);
85+
img.src = src;
86+
if (crossOrigin) img.crossOrigin = crossOrigin;
87+
if (srcSet) img.srcset = srcSet;
88+
if (sizes) img.sizes = sizes;
89+
if (loading) img.loading = loading;
8690

87-
useEffect(() => {
88-
if (!imageRef.current) return;
89-
imageRef.current.onload = (event) => {
91+
img.onload = (event) => {
9092
flush();
9193
setStatus("loaded");
9294
onLoad?.(event as unknown as ImageEvent);
9395
};
94-
imageRef.current.onerror = (error) => {
96+
img.onerror = (error) => {
9597
flush();
9698
setStatus("failed");
9799
onError?.(error as any);
98100
};
99-
}, [imageRef.current]);
101+
102+
imageRef.current = img;
103+
}, [src, crossOrigin, srcSet, sizes, onLoad, onError, loading]);
100104

101105
const flush = () => {
102106
if (imageRef.current) {
@@ -106,40 +110,25 @@ export function useImage(props: UseImageProps = {}) {
106110
}
107111
};
108112

113+
useSafeLayoutEffect(() => {
114+
/**
115+
* If user opts out of the fallback/placeholder
116+
* logic, let's bail out.
117+
*/
118+
if (ignoreFallback) return undefined;
119+
120+
if (status === "loading") {
121+
load();
122+
}
123+
124+
return () => {
125+
flush();
126+
};
127+
}, [status, load, ignoreFallback]);
128+
109129
/**
110130
* If user opts out of the fallback/placeholder
111131
* logic, let's just return 'loaded'
112132
*/
113133
return ignoreFallback ? "loaded" : status;
114134
}
115-
116-
function setImageAndGetInitialStatus(
117-
props: UseImageProps,
118-
imageRef: MutableRefObject<HTMLImageElement | null | undefined>,
119-
): Status {
120-
const {loading, src, srcSet, crossOrigin, sizes, ignoreFallback} = props;
121-
122-
if (!src) return "pending";
123-
if (ignoreFallback) return "loaded";
124-
125-
const img = new Image();
126-
127-
img.src = src;
128-
if (crossOrigin) img.crossOrigin = crossOrigin;
129-
if (srcSet) img.srcset = srcSet;
130-
if (sizes) img.sizes = sizes;
131-
if (loading) img.loading = loading;
132-
133-
imageRef.current = img;
134-
if (img.complete && img.naturalWidth) {
135-
return "loaded";
136-
}
137-
138-
return "loading";
139-
}
140-
141-
export const shouldShowFallbackImage = (status: Status, fallbackStrategy: FallbackStrategy) =>
142-
(status !== "loaded" && fallbackStrategy === "beforeLoadOrError") ||
143-
(status === "failed" && fallbackStrategy === "onError");
144-
145-
export type UseImageReturn = ReturnType<typeof useImage>;

0 commit comments

Comments
 (0)