import { useRef, useEffect, useLayoutEffect } from 'react' import PropTypes from 'prop-types' import _ from 'lodash' const SCROLL_END_OFFSET = 30 function InfiniteScroll({ atEnd, children, className = '', fetchData, itemCount, isLoading, }) { const root = useRef(null) // we keep the value in a Ref instead of state so it can be safely used in effects const scrollBottomRef = useRef(0) function setScrollBottom(value) { scrollBottomRef.current = value } function updateScrollPosition() { root.current.scrollTop = root.current.scrollHeight - root.current.clientHeight - scrollBottomRef.current } // Repositions the scroll after new items are loaded useLayoutEffect(updateScrollPosition, [itemCount]) // Repositions the scroll after a window resize useEffect(() => { const handleResize = _.debounce(updateScrollPosition, 400) window.addEventListener('resize', handleResize) return () => { window.removeEventListener('resize', handleResize) } }, []) function onScrollHandler(event) { setScrollBottom( root.current.scrollHeight - root.current.scrollTop - root.current.clientHeight ) if (event.target !== event.currentTarget) { // Ignore scroll events on nested divs // (this check won't be necessary in React 17: https://github.com/facebook/react/issues/15723 return } if (shouldFetchData()) { fetchData() } } function shouldFetchData() { const containerIsLargerThanContent = root.current.children[0].clientHeight < root.current.clientHeight if (atEnd || isLoading || containerIsLargerThanContent) { return false } else { return root.current.scrollTop < SCROLL_END_OFFSET } } return (
{children}
) } InfiniteScroll.propTypes = { atEnd: PropTypes.bool, children: PropTypes.element.isRequired, className: PropTypes.string, fetchData: PropTypes.func.isRequired, itemCount: PropTypes.number.isRequired, isLoading: PropTypes.bool, } export default InfiniteScroll