infinite-scroll.js 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. import { useRef, useEffect, useLayoutEffect } from 'react'
  2. import PropTypes from 'prop-types'
  3. import _ from 'lodash'
  4. const SCROLL_END_OFFSET = 30
  5. function InfiniteScroll({
  6. atEnd,
  7. children,
  8. className = '',
  9. fetchData,
  10. itemCount,
  11. isLoading,
  12. }) {
  13. const root = useRef(null)
  14. // we keep the value in a Ref instead of state so it can be safely used in effects
  15. const scrollBottomRef = useRef(0)
  16. function setScrollBottom(value) {
  17. scrollBottomRef.current = value
  18. }
  19. function updateScrollPosition() {
  20. root.current.scrollTop =
  21. root.current.scrollHeight -
  22. root.current.clientHeight -
  23. scrollBottomRef.current
  24. }
  25. // Repositions the scroll after new items are loaded
  26. useLayoutEffect(updateScrollPosition, [itemCount])
  27. // Repositions the scroll after a window resize
  28. useEffect(() => {
  29. const handleResize = _.debounce(updateScrollPosition, 400)
  30. window.addEventListener('resize', handleResize)
  31. return () => {
  32. window.removeEventListener('resize', handleResize)
  33. }
  34. }, [])
  35. function onScrollHandler(event) {
  36. setScrollBottom(
  37. root.current.scrollHeight -
  38. root.current.scrollTop -
  39. root.current.clientHeight
  40. )
  41. if (event.target !== event.currentTarget) {
  42. // Ignore scroll events on nested divs
  43. // (this check won't be necessary in React 17: https://github.com/facebook/react/issues/15723
  44. return
  45. }
  46. if (shouldFetchData()) {
  47. fetchData()
  48. }
  49. }
  50. function shouldFetchData() {
  51. const containerIsLargerThanContent =
  52. root.current.children[0].clientHeight < root.current.clientHeight
  53. if (atEnd || isLoading || containerIsLargerThanContent) {
  54. return false
  55. } else {
  56. return root.current.scrollTop < SCROLL_END_OFFSET
  57. }
  58. }
  59. return (
  60. <div
  61. ref={root}
  62. onScroll={onScrollHandler}
  63. className={`infinite-scroll ${className}`}
  64. >
  65. {children}
  66. </div>
  67. )
  68. }
  69. InfiniteScroll.propTypes = {
  70. atEnd: PropTypes.bool,
  71. children: PropTypes.element.isRequired,
  72. className: PropTypes.string,
  73. fetchData: PropTypes.func.isRequired,
  74. itemCount: PropTypes.number.isRequired,
  75. isLoading: PropTypes.bool,
  76. }
  77. export default InfiniteScroll