I would create an Image component that would handle it's own relevant states. Then inside this component, I would use IntersectionObserver API to tell if the image's container is visible on user's browser or not.
I would have isLoading and isInview states, isLoading will be always true until isInview updates to true.
And while isLoading is true, I would use null as src for the image and will display the placeholder.
Load only the src when container is visible on user's browser.
function Image({ src }) {
const [isLoading, setIsLoading] = useState(true);
const [isInView, setIsInView] = useState(false);
const root = useRef(); // the container
useEffect(() => {
// sets `isInView` to true until root is visible on users browser
const observer = new IntersectionObserver(onIntersection, { threshold: 0 });
observer.observe(root.current);
function onIntersection(entries) {
const { isIntersecting } = entries[0];
if (isIntersecting) { // is in view
observer.disconnect();
}
setIsInView(isIntersecting);
}
}, []);
function onLoad() {
setIsLoading((prev) => !prev);
}
return (
<div
ref={root}
className={`imgWrapper` + (isLoading ? " imgWrapper--isLoading" : "")}
>
<div className="imgLoader" />
<img className="img" src={isInView ? src : null} alt="" onLoad={onLoad} />
</div>
);
}
I would also have CSS styles that will toggle the placeholder and image's display property.
.App {
--image-height: 150px;
--image-width: var(--image-height);
}
.imgWrapper {
margin-bottom: 10px;
}
.img {
height: var(--image-height);
width: var(--image-width);
}
.imgLoader {
height: 150px;
width: 150px;
background-color: red;
}
/* container is loading, hide the img */
.imgWrapper--isLoading .img {
display: none;
}
/* container not loading, display img */
.imgWrapper:not(.imgWrapper--isLoading) .img {
display: block;
}
/* container not loading, hide placeholder */
.imgWrapper:not(.imgWrapper--isLoading) .imgLoader {
display: none;
}
Now my Parent component, will do the requests for all the image urls. It would also have its own isLoading state that when set true would display its own placeholder. When the image url's request resolves, I would then map on each url to render my Image components.
export default function App() {
const [imageUrls, setImageUrls] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchImages().then((response) => {
setImageUrls(response);
setIsLoading((prev) => !prev);
});
}, []);
const images = imageUrls.map((url, index) => <Image key={index} src={url} />);
return <div className="App">{isLoading ? "Please wait..." : images}</div>;
}
