2020-10-27 06:53:13 -04:00
|
|
|
import React, { useRef, useEffect, useLayoutEffect } from 'react'
|
2020-10-12 06:25:59 -04:00
|
|
|
import PropTypes from 'prop-types'
|
2020-10-27 06:53:13 -04:00
|
|
|
import _ from 'lodash'
|
2020-10-12 06:25:59 -04:00
|
|
|
|
|
|
|
const SCROLL_END_OFFSET = 30
|
|
|
|
|
|
|
|
function InfiniteScroll({
|
|
|
|
atEnd,
|
|
|
|
children,
|
|
|
|
className = '',
|
|
|
|
fetchData,
|
|
|
|
itemCount,
|
2021-04-27 03:52:58 -04:00
|
|
|
isLoading,
|
2020-10-12 06:25:59 -04:00
|
|
|
}) {
|
|
|
|
const root = useRef(null)
|
|
|
|
|
|
|
|
// we keep the value in a Ref instead of state so it can be safely used in effects
|
2020-10-27 06:53:13 -04:00
|
|
|
const scrollBottomRef = useRef(0)
|
2020-10-12 06:25:59 -04:00
|
|
|
function setScrollBottom(value) {
|
|
|
|
scrollBottomRef.current = value
|
|
|
|
}
|
|
|
|
|
2020-10-27 06:53:13 -04:00
|
|
|
function updateScrollPosition() {
|
|
|
|
root.current.scrollTop =
|
|
|
|
root.current.scrollHeight -
|
|
|
|
root.current.clientHeight -
|
|
|
|
scrollBottomRef.current
|
2020-10-12 06:25:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Repositions the scroll after new items are loaded
|
2020-10-27 06:53:13 -04:00
|
|
|
useLayoutEffect(updateScrollPosition, [itemCount])
|
2020-10-12 06:25:59 -04:00
|
|
|
|
|
|
|
// Repositions the scroll after a window resize
|
|
|
|
useEffect(() => {
|
2020-10-27 06:53:13 -04:00
|
|
|
const handleResize = _.debounce(updateScrollPosition, 400)
|
2020-10-12 06:25:59 -04:00
|
|
|
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 (
|
|
|
|
<div
|
|
|
|
ref={root}
|
|
|
|
onScroll={onScrollHandler}
|
|
|
|
className={`infinite-scroll ${className}`}
|
|
|
|
>
|
|
|
|
{children}
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
InfiniteScroll.propTypes = {
|
|
|
|
atEnd: PropTypes.bool,
|
|
|
|
children: PropTypes.element.isRequired,
|
|
|
|
className: PropTypes.string,
|
|
|
|
fetchData: PropTypes.func.isRequired,
|
|
|
|
itemCount: PropTypes.number.isRequired,
|
2021-04-27 03:52:58 -04:00
|
|
|
isLoading: PropTypes.bool,
|
2020-10-12 06:25:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
export default InfiniteScroll
|