-
Notifications
You must be signed in to change notification settings - Fork 3
/
LazyLoadImage.tsx
97 lines (86 loc) · 3.17 KB
/
LazyLoadImage.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import React, { useEffect, useRef, useState } from "react";
import type { LazyLoadImageProps } from "./LazyLoadImage.types";
import { Skeleton } from "@mui/material";
// TODO: add tests in browser once we are done with the migration to cypress
/**
* Lazy load wrapper for HTML image brackets. Used for lazy loading images.
* @param LazyLoadImgProps The input object of the component
* @param LazyLoadImgProps.alt string Alternative text for the image
* @param LazyLoadImgProps.autoFitSkeleton boolean Flag to enable the skeleton adjusting size to the bounding element. Default is false.
* @param LazyLoadImgProps.ImgProps object ImgProps Any other prop of the <img> component
* @param LazyLoadImgProps.src string Source of the image
*/
export default function LazyLoadImage({
alt,
autoFitSkeleton = false,
ImgProps = {},
src
}: LazyLoadImageProps) {
// state to track if the image is visible
const [isVisible, setIsVisible] = useState(false);
// states to track the status of the image load, these influence whether to display alt, or MUI Skeleton
const [isLoaded, setIsLoaded] = useState(false);
const [hasError, setHasError] = useState(false);
// state variable to set the size for the skeleton to be displayed
const [dimensions, setDimensions] = useState({ height: 0, width: 0 });
// reference to the image element
const imgRef = useRef<HTMLImageElement | null>(null);
// when the image is in view, set isVisible to true
useEffect(() => {
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
if (imgRef.current) {
const { offsetWidth, offsetHeight } = imgRef.current;
setDimensions({ height: offsetHeight, width: offsetWidth });
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
// Check if the image is already loaded from cache
useEffect(() => {
if (imgRef.current && imgRef.current.complete) {
setIsLoaded(true);
}
}, [imgRef]);
// a variable to decide whether to show the Skeleton, or the image (or the alt text in case an error happened during loading)
const showSkeleton = isVisible && !isLoaded && !hasError;
// deconstruct ImgProps and the style, to apply the needed style.display prop for the image to lazy load without issues
const { style, ...restImgProps } = ImgProps || {};
const { display, ...restStyle } = style || {};
// render the image
return (
<>
{showSkeleton ? (
<Skeleton
sx={{
borderRadius: 0,
display: "block",
height: autoFitSkeleton ? "100%" : dimensions.height,
transform: "none",
width: autoFitSkeleton ? "100%" : dimensions.width
}}
/>
) : null}
<img
alt={hasError ? alt : ""}
loading="lazy"
ref={imgRef}
src={src}
style={{
...restStyle,
display: showSkeleton ? "none" : "block"
}}
onLoad={() => setIsLoaded(true)}
onError={() => setHasError(true)}
{...restImgProps}
/>
</>
);
}