93 lines
2.5 KiB
TypeScript
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]}`
|
|
}
|
|
}
|