import { useCallback, useEffect, useRef } from "react"; function isElementInViewport(el) { var rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) /* or $(window).height() */ && rect.right <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */ ); } export const useInViewEffect = ( onEnter: () => void, onLeave: () => void, { root, rootMargin, threshold, once, }: IntersectionObserverInit & { once?: boolean } = {}, ) => { const inView = useRef(false); const target = useRef(null); const observer = useRef(null); useEffect(() => { if (!target.current) return; if (isElementInViewport(target.current)) { onEnter(); if (once) { observer.current?.unobserve(target.current); observer.current?.disconnect(); observer.current = null; } inView.current = true; } else { onLeave(); inView.current = false; } }, []); const callback: IntersectionObserverCallback = useCallback( ([entry]) => { if (entry.isIntersecting && !inView.current) { onEnter(); if (once) { observer.current?.unobserve(target.current!); observer.current?.disconnect(); observer.current = null; } } else if (!entry.isIntersecting && inView.current) { onLeave(); } inView.current = entry.isIntersecting; }, // all the props are static // eslint-disable-next-line react-hooks/exhaustive-deps [], ); const setTarget = useCallback( (node) => { if (target.current && observer.current) { observer.current.unobserve(target.current); observer.current.disconnect(); observer.current = null; } if (node) { observer.current = new IntersectionObserver(callback, { root, rootMargin, threshold, }); observer.current.observe(node); target.current = node; } }, // eslint-disable-next-line react-hooks/exhaustive-deps [target, root, rootMargin, JSON.stringify(threshold), callback], ); return [setTarget, inView.current] as const; };