import { Image, ImageProps, Skeleton } from "antd"; import { CSSProperties, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import styles from "./ImageWithFallback.module.css"; /** * Custom hook for handling lazy loading with Intersection Observer * @param options Configuration options for the hook * @returns Object containing ref and isInView state */ const useLazyLoading = ({ lazy = true, priority = false, }: { lazy?: boolean; priority?: boolean; }) => { const [isInView, setIsInView] = useState(!lazy || priority); const ref = useRef(null); useEffect(() => { // Skip lazy loading if priority is true or lazy is false if (lazy && !priority) { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsInView(true); observer.disconnect(); } }, { rootMargin: "50px", // Start loading 50px before the image comes into view threshold: 0.1, }, ); const currentRef = ref.current; if (currentRef) { observer.observe(currentRef); } // Cleanup function return () => { if (currentRef) { observer.unobserve(currentRef); } observer.disconnect(); }; } else { setIsInView(true); } }, [lazy, priority]); return { ref, isInView }; }; /** * Props for the ImageWithFallback component * @extends ImageProps from Ant Design's Image component */ interface ImageWithFallbackProps extends ImageProps { /** URL for the fallback image to display when the main image fails to load */ fallbackSrc: string; /** Border radius for the image (can be number in pixels or string like '8px' or '50%') */ borderRadius?: number | string; /** If true, loads the image immediately without lazy loading */ priority?: boolean; /** If true (default), enables lazy loading of the image */ lazy?: boolean; /** Type of placeholder to show while loading (not fully implemented) */ placeholder?: "blur" | "empty"; /** Base64 data URL for blur placeholder (not fully implemented) */ blurDataURL?: string; /** Width of the image (number for pixels, string for other units) */ width?: number | string; /** Height of the image (number for pixels, string for other units) */ height?: number | string; /** Additional styles for the loading container */ loadingContainerStyle?: CSSProperties; } /** * Enhanced image component with fallback support, lazy loading, and loading states * * Features: * - Displays a fallback image when the main image fails to load * - Supports lazy loading using Intersection Observer API * - Shows loading skeleton while the image is loading * - Handles various styling options including border radius * - Optimized with React.memo to prevent unnecessary re-renders */ const ImageWithFallback = ({ fallbackSrc, src, borderRadius, style, alt = "Image", // Default alt text for better accessibility priority = false, lazy = true, width, height, loadingContainerStyle, ...rest }: ImageWithFallbackProps) => { const [imgSrc, setImgSrc] = useState(src || fallbackSrc); const [isLoading, setIsLoading] = useState(true); const [hasError, setHasError] = useState(false); // Use the custom lazy loading hook const { ref: imageRef, isInView } = useLazyLoading({ lazy, priority }); // Default width value for consistency const defaultWidth = 90; /** * Handle image load error * Sets the fallback image and updates loading/error states */ const handleError = useCallback(() => { setImgSrc(fallbackSrc); setHasError(true); setIsLoading(false); }, [fallbackSrc]); /** * Handle successful image load * Updates loading and error states */ const handleLoad = useCallback(() => { setIsLoading(false); setHasError(false); }, []); // Update imgSrc when src or fallbackSrc props change useEffect(() => { setImgSrc(src || fallbackSrc); setIsLoading(true); setHasError(false); }, [src, fallbackSrc]); // Memoize combined style to prevent unnecessary re-renders const combinedStyle: CSSProperties = useMemo( () => ({ ...style, ...(borderRadius && { borderRadius }), }), [style, borderRadius], ); /** * Memoized skeleton loader component */ const SkeletonLoader = useMemo( () => ( ), [height, width, defaultWidth, loadingContainerStyle], ); // Memoize loading container styles for better performance const loadingContainerStyles: CSSProperties = useMemo( () => ({ zIndex: 1, borderRadius: borderRadius || "8px", height, width: width || defaultWidth, textAlign: "center", ...loadingContainerStyle, }), [borderRadius, height, width, defaultWidth, loadingContainerStyle], ); // Memoize image styles for better performance const imageStyles: CSSProperties = useMemo( () => ({ opacity: isLoading ? 0 : 1, transition: "opacity 0.3s ease-in-out", objectFit: "cover", ...combinedStyle, }), [isLoading, combinedStyle], ); // Don't render the image until it's in view (for lazy loading) if (lazy && !priority && !isInView) { return (
{SkeletonLoader}
); } return (
{/* Loading state */} {isLoading && !hasError && ( )} {/* Main image */} {alt}
); }; export default ImageWithFallback;