Last active
February 5, 2022 20:09
-
-
Save mnpenner/88f5d5d2c19b7f25bc0a4dba6c3d4a55 to your computer and use it in GitHub Desktop.
Smart-sized image
This file contains hidden or 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 {useMemo, useRef} from 'react' | |
import {useBoundingBox} from '../hooks/useBoundingBox' | |
import {sortBy} from 'lodash-es' | |
interface Size { | |
width: number | |
height: number | |
} | |
interface SrcItem extends Size { | |
url: string | |
} | |
function getAspectRatio(r: Size): number { | |
return r.width/r.height | |
} | |
type Props = Override<React.ComponentProps<'img'>, { | |
srcSet: SrcItem[] | |
}, 'src'> | |
const LazyImg: React.FC<Props> = ({srcSet,...props}) => { | |
const ref = useRef<HTMLImageElement>(null); | |
const rect = useBoundingBox(ref); | |
const src = useMemo(() => { | |
if(srcSet?.length && rect.width > 0 && rect.height > 0) { | |
const aspectRatio = getAspectRatio(rect) | |
const sources = sortBy(srcSet, [ | |
src => Math.abs(getAspectRatio(src) - aspectRatio), | |
src => src.width < rect.width, | |
src => src.height < rect.height, | |
src => { | |
const isBigger = src.width >= rect.width && src.height >= rect.height; | |
return (isBigger ? 1 : -1) * src.width * src.height; | |
}, | |
]) | |
return sources[0].url | |
} | |
return undefined; | |
}, [srcSet,rect]) | |
return <img ref={ref} src={src} {...props}/> | |
} | |
LazyImg.defaultProps = { | |
loading: 'lazy' | |
} | |
export default LazyImg; |
This file contains hidden or 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 {useLayoutEffect, useCallback, useState, Ref, useRef, RefObject, useMemo} from 'react' | |
import {debounce} from 'lodash-es' | |
interface BoundingBox { | |
left: number | |
right: number | |
bottom: number | |
top: number | |
width: number | |
height: number | |
x: number; | |
y: number; | |
} | |
export const useBoundingBox = (ref: RefObject<Element>) => { | |
const [rect, setRect] = useState<BoundingBox>(getRect(ref.current)) | |
const handleResize = useMemo(() => debounce(() => { | |
if (!ref.current) { | |
return | |
} | |
// Update client rect | |
setRect(getRect(ref.current)) | |
},50,{maxWait:1000}), [ref.current]) | |
useLayoutEffect(() => { | |
const element = ref.current | |
if (!element) { | |
return | |
} | |
// handleResize() | |
if (typeof ResizeObserver === 'function') { | |
const resizeObserver = new ResizeObserver(handleResize) | |
resizeObserver.observe(element) | |
return () => { | |
resizeObserver.disconnect() | |
} | |
} else { | |
// Browser support, remove freely | |
window.addEventListener('resize', handleResize) | |
return () => { | |
window.removeEventListener('resize', handleResize) | |
} | |
} | |
}, [ref.current]) | |
return rect | |
} | |
function getRect(element: Element|null): BoundingBox { | |
if (!element) { | |
return { | |
left: 0, | |
right: 0, | |
bottom: 0, | |
top: 0, | |
width: 0, | |
height: 0, | |
x: 0, | |
y: 0 | |
} | |
} | |
return element.getBoundingClientRect() | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment