Skip to content

Instantly share code, notes, and snippets.

@MrJackdaw
Last active February 8, 2025 23:52
Show Gist options
  • Save MrJackdaw/300c2621b8ac8c13183e3892dce2a0ce to your computer and use it in GitHub Desktop.
Save MrJackdaw/300c2621b8ac8c13183e3892dce2a0ce to your computer and use it in GitHub Desktop.
ReactJS ImageLoader Component (TSX, CSS, and example usage)
/* ImageLoader styles */
.image-loader.image-loader--rounded {
border-radius: 100%;
padding: 0.4rem;
&:hover {
animation: scale-up 250ms linear;
}
}
.image-loader,
.image-loader .caption {
display: flex;
}
.image-loader[class*="border"] {
border-style: solid;
}
import { ComponentPropsWithRef, useEffect, useRef } from "react";
import "./ImageLoader.css";
type ImageLoaderProps = {
src?: string;
animationClassName?: string;
rounded?: boolean;
} & ComponentPropsWithRef<"img">;
/* ImageLoader component: smooth entry animations for images with error fallback */
const ImageLoader = (props: ImageLoaderProps) => {
const {
src,
className = "",
animationClassName = "slide-in-down",
onClick,
rounded,
...rest
} = props;
const containerRef = useRef<HTMLElement>(null);
let cName = `image-loader ${className} ${animationClassName}`.trim();
if (rounded) cName = `image-loader--rounded ${cName}`.trim();
const loaded = useRef(false);
const loading = useRef(false);
const error = useRef(false);
const scrollOpts = { capture: true, passive: true };
const loadImageWhenInView = () => {
const exit = !src;
if (exit) return;
window.removeEventListener("scroll", loadImageWhenInView, scrollOpts);
const img = new Image();
img.onerror = () => {
error.current = true;
loaded.current = true;
loading.current = false;
};
img.onload = () => {
loaded.current = true;
loading.current = false;
};
img.src = src;
};
useEffect(() => {
if (!src) return unmount;
const { current } = containerRef;
const notInView = current ? !isInViewport(current) : false;
if (notInView) {
window.addEventListener("scroll", loadImageWhenInView, scrollOpts);
return unmount;
}
// If here, "containerRef.current" is falsy, or "forceLoad" is true
loadImageWhenInView();
return unmount;
function unmount() {
window.removeEventListener("scroll", loadImageWhenInView, true);
error.current = false;
loaded.current = false;
loading.current = true;
}
}, [src]);
if (loading.current)
return <span ref={containerRef} className="spinner--before" />;
return (
<img
{...rest}
onClick={onClick}
className={cName}
src={src}
alt={rest.alt}
/>
);
};
export default ImageLoader;
export const RoundedImg = (props: ImageLoaderProps) => (
<ImageLoader {...props} rounded />
);
/** @description Assert that component bounding rect is on-screen */
function isInViewport(elem: HTMLElement) {
if (!elem) return false;
const boundingRect = elem.getBoundingClientRect();
const { top, left, bottom, right } = boundingRect;
const { documentElement } = window.document;
return (
top >= 0 &&
left >= 0 &&
bottom <= (window.innerHeight || documentElement.clientHeight) &&
right <= (window.innerWidth || documentElement.clientWidth)
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment