infinite-scroll.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import React, { useRef, useEffect } from 'react'
  2. import PropTypes from 'prop-types'
  3. const SCROLL_END_OFFSET = 30
  4. function usePrevious(value) {
  5. const ref = useRef()
  6. useEffect(() => {
  7. ref.current = value
  8. })
  9. return ref.current
  10. }
  11. function InfiniteScroll({
  12. atEnd,
  13. children,
  14. className = '',
  15. fetchData,
  16. itemCount,
  17. isLoading
  18. }) {
  19. const root = useRef(null)
  20. const prevItemCount = usePrevious(itemCount)
  21. // we keep the value in a Ref instead of state so it can be safely used in effects
  22. const scrollBottomRef = React.useRef(0)
  23. function setScrollBottom(value) {
  24. scrollBottomRef.current = value
  25. }
  26. // position updates are not immediately applied. The DOM frequently can't calculate
  27. // element bounds after react updates, so it needs some throttling
  28. function scheduleScrollPositionUpdate(throttle) {
  29. const timeoutHandler = setTimeout(
  30. () =>
  31. (root.current.scrollTop =
  32. root.current.scrollHeight -
  33. root.current.clientHeight -
  34. scrollBottomRef.current),
  35. throttle
  36. )
  37. return () => clearTimeout(timeoutHandler)
  38. }
  39. // Repositions the scroll after new items are loaded
  40. useEffect(
  41. () => {
  42. // the first render requires a longer throttling due to slower DOM updates
  43. const scrollThrottle = prevItemCount === 0 ? 150 : 0
  44. return scheduleScrollPositionUpdate(scrollThrottle)
  45. },
  46. [itemCount, prevItemCount]
  47. )
  48. // Repositions the scroll after a window resize
  49. useEffect(() => {
  50. let clearScrollPositionUpdate
  51. const handleResize = () => {
  52. clearScrollPositionUpdate = scheduleScrollPositionUpdate(400)
  53. }
  54. window.addEventListener('resize', handleResize)
  55. return () => {
  56. window.removeEventListener('resize', handleResize)
  57. if (clearScrollPositionUpdate) {
  58. clearScrollPositionUpdate()
  59. }
  60. }
  61. }, [])
  62. function onScrollHandler(event) {
  63. setScrollBottom(
  64. root.current.scrollHeight -
  65. root.current.scrollTop -
  66. root.current.clientHeight
  67. )
  68. if (event.target !== event.currentTarget) {
  69. // Ignore scroll events on nested divs
  70. // (this check won't be necessary in React 17: https://github.com/facebook/react/issues/15723
  71. return
  72. }
  73. if (shouldFetchData()) {
  74. fetchData()
  75. }
  76. }
  77. function shouldFetchData() {
  78. const containerIsLargerThanContent =
  79. root.current.children[0].clientHeight < root.current.clientHeight
  80. if (atEnd || isLoading || containerIsLargerThanContent) {
  81. return false
  82. } else {
  83. return root.current.scrollTop < SCROLL_END_OFFSET
  84. }
  85. }
  86. return (
  87. <div
  88. ref={root}
  89. onScroll={onScrollHandler}
  90. className={`infinite-scroll ${className}`}
  91. >
  92. {children}
  93. </div>
  94. )
  95. }
  96. InfiniteScroll.propTypes = {
  97. atEnd: PropTypes.bool,
  98. children: PropTypes.element.isRequired,
  99. className: PropTypes.string,
  100. fetchData: PropTypes.func.isRequired,
  101. itemCount: PropTypes.number.isRequired,
  102. isLoading: PropTypes.bool
  103. }
  104. export default InfiniteScroll