From 0d8d88e23b1ae48586c031f327a7f07910839388 Mon Sep 17 00:00:00 2001 From: Lefrant Hugo <36001639+Hlefrant@users.noreply.github.com> Date: Thu, 2 Nov 2023 15:34:46 +0100 Subject: [PATCH] Add hook interception observer and update lazy image (#154) * Add hook interception observer and update lazy image * Fix import css --------- Co-authored-by: Hugo Co-authored-by: Willy Brauner --- .../lazyImage/LazyImage.module.less | 2 +- .../libs/components/lazyImage/LazyImage.tsx | 73 ++++++++++--------- .../libs/hooks/useIntersectionObserver.tsx | 44 +++++++++++ 3 files changed, 82 insertions(+), 37 deletions(-) create mode 100644 apps/front/src/libs/hooks/useIntersectionObserver.tsx diff --git a/apps/front/src/libs/components/lazyImage/LazyImage.module.less b/apps/front/src/libs/components/lazyImage/LazyImage.module.less index d8b22063..0128de64 100644 --- a/apps/front/src/libs/components/lazyImage/LazyImage.module.less +++ b/apps/front/src/libs/components/lazyImage/LazyImage.module.less @@ -1,4 +1,4 @@ -@import (reference) "../../references.less"; +@import (reference) "../../../references.less"; .root { width: 100%; diff --git a/apps/front/src/libs/components/lazyImage/LazyImage.tsx b/apps/front/src/libs/components/lazyImage/LazyImage.tsx index 8d6213a3..fb8459f2 100644 --- a/apps/front/src/libs/components/lazyImage/LazyImage.tsx +++ b/apps/front/src/libs/components/lazyImage/LazyImage.tsx @@ -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(null) const [lazyState, setLazyState] = useState("lazyload") + const observer = useIntersectionObserver(imageRef, {}) + const isVisible = !!observer?.isIntersecting /** * Preload one image @@ -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 ( {props?.alt} ) } - -export default LazyImage diff --git a/apps/front/src/libs/hooks/useIntersectionObserver.tsx b/apps/front/src/libs/hooks/useIntersectionObserver.tsx new file mode 100644 index 00000000..4b71e0df --- /dev/null +++ b/apps/front/src/libs/hooks/useIntersectionObserver.tsx @@ -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, + { threshold = 0, root = null, rootMargin = "0%", freezeOnceVisible = false }: Args +): IntersectionObserverEntry | undefined { + const [entry, setEntry] = useState() + + 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