1+ import { RefObject , useCallback , useEffect , useMemo , useRef } from "react" ;
2+ import { useSyncExternalStore } from "." ;
3+
4+ /**
5+ * **`useInfiniteScroll`**: Hook to deal with large sets of data. It allow users to scroll through content endlessly without explicit pagination or loading new pages.
6+ * @param {Object } param
7+ * @param {(data?: T | undefined) => Promise<T> } param.request - request to obtain data.
8+ * @param {RefObject<E extends Element> } param.ref - a reference to container element.
9+ * @param {(data?: T | undefined) => boolean } param.hasMoreData - function that will be executed every time _data_ changes to detect if there will be new data values.
10+ * @param {number|undefined } [param.threshold=0] - a threshold value by which load next data during scroll.
11+ * @param {()=>void } [param.onBefore] - function that will be executed before to execute __request__.
12+ * @param {()=>void } [param.onSuccess] - function that will be executed if __request__ execution has success.
13+ * @param {(err:unknown)=>void } [param.onError] - function that will be executed if an error occurred calling __request__.
14+ * @returns {{data: T|undefined, loading: boolean, fullData: boolean, updateData: (data:T|((currentState?:T)=>T))=>void, loadData: ()=>Promise<void>} } result
15+ * Object with these properties:
16+ * - __data__: data returned from _request_ execution.
17+ * - __loading__: boolean that will be true if a _request_ execution is in pending, otherwise it will be false.
18+ * - __fullData__: boolean that indicates if all data are returned or not.
19+ * - __updateData__: function to update data from outside.
20+ * - __loadData__: function to manual load next data.
21+ */
22+ export const useInfiniteScroll = < T , E extends Element > ( { request, ref, hasMoreData, threshold, onBefore, onError, onSuccess } : { request : ( data ?: T ) => Promise < T > , ref : RefObject < E > , hasMoreData : ( data ?: T ) => boolean , threshold ?: number , onBefore ?: ( ) => void , onSuccess ?: ( ) => void , onError ?: ( err : unknown ) => void } ) : { data : T | undefined , loading : boolean , fullData : boolean , updateData : ( data : T | ( ( currentState ?: T ) => T ) ) => void , loadData : ( ) => Promise < void > } => {
23+ const notifyRef = useRef < ( ) => void > ( ) ;
24+ const cachedState = useRef < { data ?: T , loading : boolean , fullData : boolean } > ( {
25+ data : undefined ,
26+ loading : true ,
27+ fullData : ! hasMoreData ( ) ,
28+ } ) ;
29+
30+ const loadData = useCallback ( ( ) => {
31+ if ( cachedState . current . loading ) {
32+ return Promise . resolve ( ) ;
33+ }
34+ cachedState . current . loading = true ;
35+ ! ! notifyRef . current && notifyRef . current ( ) ;
36+ ! ! onBefore && onBefore ( ) ;
37+ return request ( cachedState . current . data )
38+ . then ( data => {
39+ cachedState . current . data = data ;
40+ cachedState . current . loading = false ;
41+ cachedState . current . fullData = ! hasMoreData ( data ) ;
42+ ! ! notifyRef . current && notifyRef . current ( ) ;
43+ ! ! onSuccess && onSuccess ( ) ;
44+ } )
45+ . catch ( err => {
46+ cachedState . current . loading = false ;
47+ ! ! notifyRef . current && notifyRef . current ( ) ;
48+ ! ! onError && onError ( err ) ;
49+ } ) ;
50+ } , [ onBefore , onSuccess , onError , request , hasMoreData ] ) ;
51+
52+ const loadOnScroll = useCallback ( ( el : Element ) => {
53+ if ( cachedState . current . loading || cachedState . current . fullData ) {
54+ return
55+ }
56+ const scrollTop = el === ( document . documentElement as Element )
57+ ? Math . max (
58+ window . scrollY ,
59+ ( document . documentElement as Element ) . scrollTop ,
60+ document . body . scrollTop ,
61+ )
62+ : el . scrollTop ;
63+ const scrollHeight = el . scrollHeight || Math . max ( document . documentElement . scrollHeight , document . body . scrollHeight ) ;
64+ const clientHeight = el . clientHeight || Math . max ( document . documentElement . clientHeight , document . body . clientHeight ) ;
65+
66+ ( scrollHeight - scrollTop <= clientHeight + ( threshold ?? 0 ) ) && loadData ( ) ;
67+ } , [ loadData , threshold ] ) ;
68+
69+ const updateData = useCallback ( ( data : T | ( ( currentState ?:T ) => T ) ) => {
70+ cachedState . current . data = data instanceof Function ? data ( cachedState . current . data ) : data ;
71+ ! ! notifyRef . current && notifyRef . current ( ) ;
72+ } , [ ] ) ;
73+
74+ const state = useSyncExternalStore (
75+ useCallback ( notif => {
76+ notifyRef . current = notif ;
77+ const el = ref . current as Element ;
78+ const onScroll = ( ) => loadOnScroll ( el ) ;
79+ el && el . addEventListener ( "scroll" , onScroll , { passive : true } ) ;
80+ return ( ) => {
81+ notifyRef . current = undefined ;
82+ el && el . removeEventListener ( "scroll" , onScroll ) ;
83+ }
84+ } , [ loadOnScroll , ref ] ) ,
85+ useMemo ( ( ) => {
86+ let state : typeof cachedState . current = {
87+ loading : true ,
88+ fullData : cachedState . current . fullData
89+ } ;
90+ return ( ) => {
91+ if ( state . data !== cachedState . current . data ) {
92+ state . data = ( cachedState . current . data === undefined
93+ ? undefined
94+ : Array . isArray ( cachedState . current . data )
95+ ? [ ...cachedState . current . data ]
96+ : typeof cachedState . current . data === "object"
97+ ? { ...cachedState . current . data }
98+ : cachedState . current . data ) as T | undefined ;
99+ state = { ...cachedState . current } ;
100+ }
101+ if ( state . loading !== cachedState . current . loading || state . fullData !== cachedState . current . fullData ) {
102+ state = { ...cachedState . current } ;
103+ }
104+ return state ;
105+ }
106+ } , [ ] )
107+ ) ;
108+
109+ useEffect ( ( ) => {
110+ ! ! onBefore && onBefore ( ) ;
111+ cachedState . current . loading = true ;
112+ request ( )
113+ . then ( data => {
114+ cachedState . current . data = data ;
115+ cachedState . current . loading = false ;
116+ cachedState . current . fullData = ! hasMoreData ( data ) ;
117+ ! ! notifyRef . current && notifyRef . current ( ) ;
118+ ! ! onSuccess && onSuccess ( ) ;
119+ } )
120+ . catch ( err => {
121+ cachedState . current . loading = false ;
122+ ! ! notifyRef . current && notifyRef . current ( ) ;
123+ ! ! onError && onError ( err ) ;
124+ } )
125+ } , [ request , onError , onBefore , onSuccess , hasMoreData ] ) ;
126+
127+ return {
128+ data : state . data ,
129+ loading : state . loading ,
130+ fullData : state . fullData ,
131+ updateData,
132+ loadData
133+ }
134+ }
0 commit comments