import { useCallback, useEffect, useMemo, useRef, useState } from "react"; interface ScrollTrackerResult { isScrollable: boolean; isAtTop: boolean; isAtBottom: boolean; } function useScrollTracker(ref: React.RefObject): ScrollTrackerResult { const [isScrollable, setIsScrollable] = useState(false); const [isAtTop, setIsAtTop] = useState(true); const [isAtBottom, setIsAtBottom] = useState(false); const getScrollState = useCallback(() => { if (ref.current) { const { scrollTop, scrollHeight, clientHeight } = ref.current; setIsScrollable(scrollHeight > clientHeight); setIsAtTop(scrollTop === 0); setIsAtBottom(scrollTop + clientHeight >= scrollHeight); } }, [ref]); const debouncedGetScrollState = useRef(debounce(getScrollState, 100)); useEffect(() => { const element = ref.current; if (element) { element.addEventListener("scroll", debouncedGetScrollState.current); return () => element.removeEventListener("scroll", debouncedGetScrollState.current); } }, [debouncedGetScrollState]); const state: ScrollTrackerResult = useMemo(() => ({ isScrollable, isAtTop, isAtBottom }), [isScrollable, isAtTop, isAtBottom]); return state; } function debounce(fn: () => any, delayMs = 100) { let timeout: any; return () => { clearTimeout(timeout); timeout = setTimeout(fn, delayMs); } } export default useScrollTracker;