Skip to content

Commit

Permalink
Add hook interception observer and update lazy image (#154)
Browse files Browse the repository at this point in the history
* Add hook interception observer and update lazy image

* Fix import css

---------

Co-authored-by: Hugo <hugo@cher-ami.tv>
Co-authored-by: Willy Brauner <willybrauner@gmail.com>
  • Loading branch information
3 people authored Nov 2, 2023
1 parent fbac0f1 commit 0d8d88e
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@import (reference) "../../references.less";
@import (reference) "../../../references.less";

.root {
width: 100%;
Expand Down
73 changes: 37 additions & 36 deletions apps/front/src/libs/components/lazyImage/LazyImage.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
import React, { CSSProperties, useEffect, useRef, useState } from "react"
import React, { CSSProperties, useRef, useState } from "react"
import css from "./LazyImage.module.less"
import { cls } from "@cher-ami/utils"
import useIntersectionObserver from "~/libs/hooks/useIntersectionObserver"
import { useAsyncEffect } from "~/libs/hooks/useAsyncEffect"

interface IProps {
type TSrc = {
dataSrc: string
}

type TSrcset = {
dataSrcset: string
}

type TProps = {
alt: string
src?: string
dataSrc?: string
dataSrcset?: string
className?: string
alt?: string
aspectRatio?: number
style?: CSSProperties
onLoaded?: (img: HTMLImageElement) => void
}
} & (TSrc | TSrcset)

export type Lazy = "lazyload" | "lazyloading" | "lazyloaded"

/**
* @name LazyImage
*/
function LazyImage(props: IProps) {
export function LazyImage(props: TProps) {
const imageRef = useRef<HTMLImageElement>(null)
const [lazyState, setLazyState] = useState<Lazy>("lazyload")
const observer = useIntersectionObserver(imageRef, {})
const isVisible = !!observer?.isIntersecting

/**
* Preload one image
Expand All @@ -43,43 +53,36 @@ function LazyImage(props: IProps) {
/**
* Intersection observer
*/
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(async (entry) => {
if (entry.isIntersecting) {
const image = entry.target as HTMLImageElement
if (lazyState === "lazyloaded") return
setLazyState("lazyloading")
useAsyncEffect(async () => {
if (isVisible) {
if (lazyState === "lazyloaded") return

// Start preload
await preloadImage(image)
setLazyState("lazyloading")

// Set src & srcset, then remove data-attr on DOM
if (image.dataset.src) image.src = image.dataset.src
if (image.dataset.srcset) image.srcset = image.dataset.srcset
image.removeAttribute("data-src")
image.removeAttribute("data-srcset")
// Start preload
await preloadImage(imageRef.current)

// end!
setLazyState("lazyloaded")
observer.unobserve(image)
props.onLoaded?.(image)
}
})
})
if (imageRef.current) observer.observe(imageRef.current)
return () => {
if (imageRef.current) observer.unobserve(imageRef.current)
// Set src & srcset, then remove data-attr on DOM
if (imageRef.current.dataset.src)
imageRef.current.src = imageRef.current.dataset.src
if (imageRef.current.dataset.srcset)
imageRef.current.srcset = imageRef.current.dataset.srcset
imageRef.current.removeAttribute("data-src")
imageRef.current.removeAttribute("data-srcset")

// end!
setLazyState("lazyloaded")
props.onLoaded?.(imageRef.current)
}
}, [])
}, [isVisible])

return (
<img
ref={imageRef}
className={cls(css.root, props.className, lazyState)}
src={props.src ?? "data:,"}
data-src={props?.dataSrc}
data-srcset={props?.dataSrcset}
data-src={(props as TProps & TSrc)?.dataSrc}
data-srcset={(props as TProps & TSrcset)?.dataSrcset}
alt={props?.alt}
style={{
...(props.aspectRatio ? { aspectRatio: `${props.aspectRatio}` } : {}),
Expand All @@ -88,5 +91,3 @@ function LazyImage(props: IProps) {
/>
)
}

export default LazyImage
44 changes: 44 additions & 0 deletions apps/front/src/libs/hooks/useIntersectionObserver.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { RefObject, useEffect, useState } from "react"

export interface Args extends IntersectionObserverInit {
freezeOnceVisible?: boolean
}

/**
* useIntersectionObserver
* https://usehooks-ts.com/react-hook/use-intersection-observer
*
* @param elementRef
* @param threshold
* @param root
* @param rootMargin
* @param freezeOnceVisible
*/
function useIntersectionObserver(
elementRef: RefObject<Element>,
{ threshold = 0, root = null, rootMargin = "0%", freezeOnceVisible = false }: Args
): IntersectionObserverEntry | undefined {
const [entry, setEntry] = useState<IntersectionObserverEntry>()

const frozen = entry?.isIntersecting && freezeOnceVisible

const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
setEntry(entry)
}
useEffect(() => {
const node = elementRef?.current // DOM Ref
const hasIOSupport = !!window.IntersectionObserver

if (!hasIOSupport || frozen || !node) return

const observerParams = { threshold, root, rootMargin }
const observer = new IntersectionObserver(updateEntry, observerParams)
observer.observe(node)

return () => observer.disconnect()
}, [elementRef, JSON.stringify(threshold), root, rootMargin, frozen])

return entry
}

export default useIntersectionObserver

0 comments on commit 0d8d88e

Please sign in to comment.