infinite-scroll.tsx 2.3 KB

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