Last active
January 31, 2023 23:24
-
-
Save akramsaouri/533b90326a811ed7adb75062e069b6a9 to your computer and use it in GitHub Desktop.
⚛️ Lazy load images in React with IntersectionObserver.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useEffect, useRef, FunctionComponent, HTMLProps } from 'react'; | |
import 'intersection-observer'; // if you want to include a polyfill | |
/** | |
* Returns an IntersectionObserver that loads the image | |
* when it is at least {threshold}*100 visible in the viewport. | |
* | |
* PS: Cached on the window for performance | |
*/ | |
function getImageLoaderObserver( | |
ioOptions: IntersectionObserverInit = {}, | |
): null | IntersectionObserver { | |
const { threshold = 0.01 } = ioOptions; | |
// cache key is dependent of io args | |
const CACHE_KEY = `__LAZY_IMAGE_IO_${JSON.stringify(ioOptions)}`; | |
if (typeof IntersectionObserver !== 'function') { | |
return null; | |
} | |
// return the cached observer for performance | |
if (typeof window[CACHE_KEY] !== 'undefined') { | |
return window[CACHE_KEY]; | |
} | |
// create a new observer and cache it on the window | |
window[CACHE_KEY] = new IntersectionObserver( | |
entries => { | |
entries.forEach(entry => { | |
if (entry.isIntersecting) { | |
const img = entry.target as HTMLImageElement; | |
const dataSrc = img.getAttribute('data-src'); | |
if (dataSrc) { | |
const parent = img.parentNode as HTMLElement; | |
if (parent?.nodeName === 'PICTURE') { | |
// handles special <picture><source> case | |
Array.from(parent.getElementsByTagName('source')).forEach( | |
(source: HTMLSourceElement) => { | |
const dataSrcSet = source.getAttribute('data-srcset'); | |
if (!img.hasAttribute('srcset') && dataSrcSet) { | |
source.setAttribute('srcset', dataSrcSet); | |
} | |
}, | |
); | |
} | |
if (!img.hasAttribute('src') || dataSrc !== img.getAttribute('src')) { | |
img.setAttribute('src', dataSrc); | |
} | |
} | |
} | |
}); | |
}, | |
{ | |
...ioOptions, | |
threshold, | |
}, | |
); | |
return window[CACHE_KEY]; | |
} | |
export interface LazyImageProps extends HTMLProps<HTMLImageElement> { | |
className?: string; | |
src: string; | |
alt?: string; | |
errorSrc?: string; | |
ioOptions?: IntersectionObserverInit; | |
onError?: () => void; | |
onClick?: (e: any) => void; | |
} | |
const LazyImage: FunctionComponent<LazyImageProps> = ({ | |
className, | |
src, | |
alt, | |
ioOptions = {}, | |
errorSrc, | |
onError, | |
onClick, | |
style, | |
}: LazyImageProps) => { | |
const imgRef = useRef<HTMLImageElement | null>(null); | |
const handleError = () => { | |
// manually attach fallback src to img on Error | |
if (imgRef.current && errorSrc) { | |
imgRef.current.src = errorSrc; | |
onError && onError(); | |
} | |
}; | |
useEffect(() => { | |
const observer = getImageLoaderObserver(ioOptions); | |
const target = imgRef.current; | |
if (observer && target) { | |
observer.observe(target); | |
} | |
return () => { | |
if (observer && target) { | |
observer.unobserve(target); | |
} | |
}; | |
}, [src]); | |
return ( | |
<img | |
className={className} | |
data-src={src} | |
ref={imgRef} | |
onError={handleError} | |
alt={alt} | |
onClick={onClick} | |
style={style} | |
/> | |
); | |
}; | |
export default LazyImage; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment