Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hook interception observer and update lazy image #154

Merged
merged 2 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading