11// @ts -nocheck
22import h from "@macrostrat/hyper" ;
33import update , { Spec } from "immutability-helper" ;
4- import React , { useReducer , useEffect , useRef , useCallback , memo } from "react" ;
4+ import React , { useReducer , useEffect , useRef , useCallback } from "react" ;
55import { Spinner , NonIdealState } from "@blueprintjs/core" ;
66import { APIParams , QueryParams } from "./util/query-string" ;
77import { useInView } from "react-intersection-observer" ;
@@ -20,44 +20,15 @@ interface ScrollState<T = object> {
2020 pageIndex : number ;
2121}
2222
23- type ScrollResponseItems < T > = Pick <
24- ScrollState < T > ,
25- "count" | "hasMore" | "items"
26- > ;
27-
28- interface InfiniteScrollProps < T > extends Omit < APIResultProps < T > , "params" > {
29- getCount ( r : T ) : number ;
30- getNextParams ( r : T , params : QueryParams ) : QueryParams ;
31- getItems ( r : T ) : any ;
32- hasMore ( res : T ) : boolean ;
33- totalCount ?: number ;
34- // Only allow more restrictive parameter types
35- params : APIParams ;
36- className ?: string ;
37- itemComponent ?: React . ComponentType < { data : T ; index : number } > ;
38- loadingPlaceholder ?: React . ComponentType ;
39- emptyPlaceholder ?: React . ComponentType ;
40- finishedPlaceholder ?: React . ComponentType ;
41- resultsComponent ?: React . ComponentType < { data : T [ ] } > ;
42- perPage ?: number ;
43- startPage ?: number ;
44- initialData ?: T [ ] ; // to allow for server-side rendering for initial state
45- }
46-
4723type UpdateState < T > = { type : "update-state" ; spec : Spec < ScrollState < T > > } ;
48- type LoadNextPage = {
49- type : "load-next-page" ;
50- page : number ;
51- } ;
5224type LoadPage < T > = {
5325 type : "load-page" ;
5426 params : APIParams ;
5527 dispatch : Dispatch < T > ;
5628 callback < T > ( action : LoadPage < T > ) : void ;
5729} ;
5830
59- type ScrollAction < T > = UpdateState < T > | LoadNextPage | LoadPage < T > ;
60-
31+ type ScrollAction < T > = UpdateState < T > | LoadPage < T > ;
6132type Reducer < T > = (
6233 state : ScrollState < T > ,
6334 action : ScrollAction < T > ,
@@ -74,7 +45,6 @@ function infiniteScrollReducer<T>(
7445 case "load-page" :
7546 action . callback ( action ) ;
7647 return update ( state , {
77- // @ts -ignore
7848 isLoadingPage : { $set : action . params . page ?? 0 } ,
7949 } ) ;
8050 }
@@ -88,22 +58,25 @@ export function InfiniteScroll(props) {
8858 loadMore,
8959 offset = 0 ,
9060 isLoading,
61+ delay = 100 ,
9162 } = props ;
9263 const { ref, inView } = useInView ( {
9364 rootMargin : `0px 0px ${ offset } px 0px` ,
9465 trackVisibility : true ,
95- delay : 100 ,
66+ delay : delay >= 100 ? delay : 100 ,
9667 } ) ;
9768
98- const shouldLoadMore = hasMore && inView ;
69+ // Only load more if not currently loading
70+ const shouldLoadMore = hasMore && inView && ! isLoading ;
9971
10072 useEffect ( ( ) => {
101- if ( shouldLoadMore ) loadMore ( ) ;
102- } , [ shouldLoadMore , isLoading ] ) ;
73+ if ( shouldLoadMore ) {
74+ loadMore ( ) ;
75+ }
76+ } , [ shouldLoadMore , loadMore ] ) ;
10377
10478 return h ( "div.infinite-scroll-container" , { className } , [
10579 children ,
106- //h.if(state.isLoadingPage != null)(placeholder),
10780 h ( "div.bottom-marker" , { ref, style : { padding : "1px" } } ) ,
10881 ] ) ;
10982}
@@ -170,11 +143,6 @@ function FinishedPlaceholder({ totalCount, ...rest }: { totalCount?: number }) {
170143}
171144
172145function InfiniteScrollView < T > ( props : InfiniteScrollProps < T > ) {
173- /*
174- A container for cursor-based pagination. This is built for
175- the GeoDeepDive API right now, but it can likely be generalized
176- for other uses.
177- */
178146 const {
179147 route,
180148 params,
@@ -189,6 +157,7 @@ function InfiniteScrollView<T>(props: InfiniteScrollProps<T>) {
189157 perPage = 10 ,
190158 startPage = 0 ,
191159 initialItems = [ ] ,
160+ delay,
192161 } = props ;
193162 const { get } = useAPIActions ( ) ;
194163 const { getCount, getNextParams, getItems, hasMore } = props ;
@@ -203,88 +172,96 @@ function InfiniteScrollView<T>(props: InfiniteScrollProps<T>) {
203172 pageIndex : startPage ,
204173 } ;
205174
206- const pageOffset = 0 ;
207-
208175 const [ state , dispatch ] = useReducer < Reducer < T > > (
209176 infiniteScrollReducer ,
210177 initialState ,
211178 ) ;
212179
180+ const loadingRef = useRef ( false ) ;
181+
182+ const mountedRef = useRef ( true ) ;
183+ useEffect ( ( ) => {
184+ return ( ) => {
185+ mountedRef . current = false ;
186+ } ;
187+ } , [ ] ) ;
188+
213189 const loadPage = useCallback (
214190 async ( action : LoadPage < T > ) => {
215- const res = await get ( route , action . params , opts ) ;
216- const itemVals = getItems ( res ) ;
217- const ival = { $push : itemVals } ;
218- const nextLength = state . items . length + itemVals . length ;
219- const count = getCount ( res ) ;
220- // if (state.isLoadingPage == null) {
221- // // We have externally cancelled this request (by e.g. moving to a new results set)
222- // console.log("Loading cancelled")
223- // return
224- // }
225-
226- let p1 : QueryParams = getNextParams ( res , params ) ;
227- let hasNextParams = p1 != null ;
228-
229- action . dispatch ( {
230- type : "update-state" ,
231- spec : {
232- items : ival ,
233- // @ts -ignore
234- scrollParams : { $set : p1 } ,
235- pageIndex : { $set : state . pageIndex + 1 } ,
236- count : { $set : count } ,
237- hasMore : {
238- $set : hasMore ( res ) && itemVals . length > 0 && hasNextParams ,
191+ if ( loadingRef . current ) return ; // Prevent concurrent loads
192+ loadingRef . current = true ;
193+
194+ dispatch ( action ) ;
195+
196+ try {
197+ const res = await get ( route , action . params , opts ) ;
198+ if ( ! mountedRef . current ) return ;
199+
200+ const itemVals = getItems ( res ) ;
201+ const nextParams = getNextParams ( res , action . params ) ;
202+ const count = getCount ( res ) ;
203+ const more = hasMore ( res ) && itemVals . length > 0 && nextParams != null ;
204+
205+ action . dispatch ( {
206+ type : "update-state" ,
207+ spec : {
208+ items : { $push : itemVals } ,
209+ scrollParams : { $set : nextParams } ,
210+ pageIndex : { $set : state . pageIndex + 1 } ,
211+ count : { $set : count } ,
212+ hasMore : { $set : more } ,
213+ isLoadingPage : { $set : null } ,
214+ error : { $set : null } ,
239215 } ,
240- isLoadingPage : { $set : null } ,
241- } ,
242- } ) ;
216+ } ) ;
217+ } catch ( error ) {
218+ if ( ! mountedRef . current ) return ;
219+ action . dispatch ( {
220+ type : "update-state" ,
221+ spec : { error : { $set : error } , isLoadingPage : { $set : null } } ,
222+ } ) ;
223+ } finally {
224+ loadingRef . current = false ;
225+ }
243226 } ,
244- [ state . items , route , params , opts ] ,
227+ [
228+ get ,
229+ route ,
230+ opts ,
231+ getItems ,
232+ getNextParams ,
233+ getCount ,
234+ hasMore ,
235+ state . pageIndex ,
236+ ] ,
245237 ) ;
246238
247239 const loadMore = useCallback ( ( ) => {
240+ if ( state . isLoadingPage !== null || ! state . hasMore ) return ;
248241 dispatch ( {
249242 type : "load-page" ,
250243 params : state . scrollParams ,
251244 dispatch,
252- // @ts -ignore
253245 callback : loadPage ,
254246 } ) ;
255- } , [ state . scrollParams , loadPage , route , params , opts ] ) ;
247+ } , [ state . isLoadingPage , state . hasMore , state . scrollParams , loadPage ] ) ;
256248
257249 const isInitialRender = useRef ( true ) ;
258- const loadInitialData = useCallback (
259- function ( ) {
260- // Don't run on initial render
261- if ( isInitialRender . current ) {
262- isInitialRender . current = false ;
263- return ;
264- }
265- /*
266- Get the initial dataset
267- */
268- // const success = await get(route, params, opts);
269- // parseResponse(success, true)
270- //if (state.items.length == 0 && state.isLoadingPage == null) return
271- dispatch ( { type : "update-state" , spec : { $set : initialState } } ) ;
272- //await loadNext(0)
273- } ,
274- [ isInitialRender , route , params , opts ] ,
275- ) ;
276250
277- useEffect ( loadInitialData , [ props . route , props . params ] ) ;
251+ useEffect ( ( ) => {
252+ if ( isInitialRender . current ) {
253+ isInitialRender . current = false ;
254+ if ( state . items . length === 0 ) {
255+ loadMore ( ) ;
256+ }
257+ }
258+ } , [ loadMore , state . items . length ] ) ;
278259
279260 if ( state == null ) return null ;
280261
281- //useAsyncEffect(getInitialData, [route, params]);
282-
283- //const showLoader = state.isLoadingPage != null && state.items.length > 0
284-
285262 const data = state . items ;
286263 const isLoading = state . isLoadingPage != null ;
287- const isEmpty = data . length == 0 && ! isLoading ;
264+ const isEmpty = data . length === 0 && ! isLoading ;
288265 const isFinished = ! state . hasMore && ! isLoading ;
289266 const totalCount = props . totalCount ?? state . count ;
290267
@@ -297,40 +274,38 @@ function InfiniteScrollView<T>(props: InfiniteScrollProps<T>) {
297274 loader : placeholder ,
298275 useWindow : true ,
299276 className,
277+ delay,
278+ isLoading,
300279 } ,
301280 [
302281 h . if ( isEmpty ) ( emptyPlaceholder ) ,
303282 h . if ( ! isEmpty ) ( IndexingProvider , { totalCount, indexOffset : 0 } , [
304283 h (
305284 resultsComponent ,
306285 { data } ,
307- data . map ( ( d , i ) => {
308- return h ( itemComponent , { key : i , data : d , index : i } ) ;
309- } ) ,
286+ data . map ( ( d , i ) => h ( itemComponent , { key : i , data : d , index : i } ) ) ,
310287 ) ,
311- // @ts -ignore
312288 h . if ( isLoading ) ( loadingPlaceholder , {
313289 totalCount,
314290 scrollParams : state . scrollParams ,
315291 pageIndex : state . pageIndex ,
316292 loadedCount : data . length ,
317293 perPage,
318294 } ) ,
319- // @ts -ignore
320295 h . if ( isFinished ) ( finishedPlaceholder , { totalCount } ) ,
321296 ] ) ,
322297 ] ,
323298 ) ;
324299}
325300
326301InfiniteScrollView . defaultProps = {
327- hasMore ( res ) {
302+ hasMore ( ) {
328303 return true ;
329304 } ,
330305 getItems ( d ) {
331306 return d ;
332307 } ,
333- getCount ( d ) {
308+ getCount ( ) {
334309 return null ;
335310 } ,
336311 getNextParams ( response , params ) {
0 commit comments