-
Notifications
You must be signed in to change notification settings - Fork 22
/
Copy pathuseInfiniteScroll.ts
115 lines (102 loc) · 3.22 KB
/
useInfiniteScroll.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import { RefCallback, useCallback, useEffect, useState } from 'react'
import { useOnScreen } from './useOnScreen'
import { useUpdatingRef } from './useUpdatingRef'
export type UseInfiniteScrollOptions = {
/**
* The callback to execute when moving to the next page.
*/
loadMore: () => void
/**
* Optionally specify an element instead of using the ref in the return value.
*/
element?: HTMLElement | null
/**
* The infinite scroll factor is how close to the bottom the user has to be to
* load more. 0 triggers loading when scrolled all the way to the bottom, and
* 0.5 triggers loading when the user has half the screen remaining before the
* bottom. 1 will ensure there is at least a screen's height worth of items
* are loaded.
*
* Defaults to 1. Disabled if negative.
*/
infiniteScrollFactor?: number
/**
* Disable infinite scroll.
*/
disabled?: boolean
}
export type UseInfiniteScrollReturn = {
/**
* The ref to set on the element you want to track.
*/
infiniteScrollRef: RefCallback<HTMLElement>
}
/**
* A hook that triggers a callback when the user scrolls to the bottom of an
* element, as determined by the bottom of the element reaching a certain
* threshold below the bottom of the window. This element should NOT be the
* scrollable container itself, but instead the element inside the container
* that contains the content.
*
* The returned `infiniteScrollRef` should be set on the container element.
*/
export const useInfiniteScroll = ({
loadMore,
element: _element,
infiniteScrollFactor = 1,
disabled,
}: UseInfiniteScrollOptions): UseInfiniteScrollReturn => {
const [element, setElement] = useState<HTMLElement | null>(_element || null)
const infiniteScrollRef = useCallback(
(node: HTMLElement | null) => setElement(node),
[setElement]
)
const isVisible = useOnScreen(element)
// Use _element from options if exists.
useEffect(() => {
if (_element !== undefined && element !== _element) {
setElement(_element)
}
}, [element, _element])
// Memoize loadMore in case it changes between renders.
const loadMoreRef = useUpdatingRef(loadMore)
useEffect(() => {
if (
disabled ||
!element ||
!isVisible ||
infiniteScrollFactor < 0 ||
typeof window === 'undefined'
) {
return
}
let executedLoadingMore = false
const onScroll = () => {
if (executedLoadingMore) {
return
}
// Check if container is near the bottom.
const { bottom } = element.getBoundingClientRect()
if (
bottom - window.innerHeight * infiniteScrollFactor <=
window.innerHeight
) {
loadMoreRef.current()
// Prevent spamming load more function.
executedLoadingMore = true
setTimeout(() => {
executedLoadingMore = false
onScroll()
}, 250)
}
}
onScroll()
// Set third argument to `true` to capture all scroll events instead of
// waiting for them to bubble up.
window.addEventListener('scroll', onScroll, true)
return () => window.removeEventListener('scroll', onScroll, true)
}, [infiniteScrollFactor, disabled, element, loadMoreRef, isVisible])
return {
infiniteScrollRef,
}
}