femto-webapp/src/hooks/useIntersectionLoad.ts
2025-05-06 16:31:55 +02:00

93 lines
2.5 KiB
TypeScript

import { RefObject, useEffect, useRef } from 'react'
interface UseIntersectionLoadOptions extends IntersectionObserverInit {
earlyTriggerPx?: number
debounceMs?: number
}
export function useIntersectionLoad(
callback: () => Promise<void>,
elementRef: RefObject<HTMLElement | null>,
{
earlyTriggerPx = 1200,
debounceMs = 300,
root = null,
threshold = 0.1,
rootMargin = '0px',
}: UseIntersectionLoadOptions = {},
) {
const observerRef = useRef<IntersectionObserver | null>(null)
const loading = useRef(false)
const timeoutRef = useRef<number | null>(null)
useEffect(() => {
const el = elementRef.current
if (!el) return
const margin = computeAdjustedRootMargin(rootMargin, earlyTriggerPx)
const debouncedCallback = () => {
if (timeoutRef.current) return
timeoutRef.current = setTimeout(async () => {
timeoutRef.current = null
if (!loading.current) {
loading.current = true
try {
await callback()
} finally {
loading.current = false
if (elementRef.current && observerRef.current) {
observerRef.current.unobserve(elementRef.current)
observerRef.current.observe(elementRef.current)
}
}
}
}, debounceMs)
}
observerRef.current = new IntersectionObserver(
(entries) => {
const entry = entries[0]
if (entry.isIntersecting) {
debouncedCallback()
}
},
{
root,
threshold,
rootMargin: margin,
},
)
observerRef.current.observe(el)
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}
if (el && observerRef.current) {
observerRef.current.unobserve(el)
observerRef.current.disconnect()
}
}
}, [callback, elementRef, root, rootMargin, threshold, earlyTriggerPx, debounceMs])
}
// Utility to adjust rootMargin's top value
function computeAdjustedRootMargin(baseMargin: string, extraTopPx: number): string {
const parts = baseMargin.split(' ')
const top = parts[0] || '0px'
const adjustedTop = `${parseInt(top, 10) + extraTopPx}px`
// Maintain format: top [right bottom left]
if (parts.length === 1) {
return `${adjustedTop}`
} else if (parts.length === 2) {
return `${adjustedTop} ${parts[1]}`
} else if (parts.length === 3) {
return `${adjustedTop} ${parts[1]} ${parts[2]}`
} else {
return `${adjustedTop} ${parts[1]} ${parts[2]} ${parts[3]}`
}
}