Skip to content

Commit 9ecf536

Browse files
authored
Merge pull request #115 from UW-Macrostrat/infinitescroll
InfiniteScrollView delay and duplicate data
2 parents 359c560 + c5f83ba commit 9ecf536

File tree

4 files changed

+104
-221
lines changed

4 files changed

+104
-221
lines changed

packages/ui-components/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## [4.3.1] - 2025-07-02
4+
5+
- Updated infinite scorlling to take `delay` as input param (default 100ms)
6+
- Fixed duplicate data issue in Strict Mode
7+
38
## [4.3.0] - 2025-06-25
49

510
Updated infinite scrolling

packages/ui-components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@macrostrat/ui-components",
3-
"version": "4.3.0",
3+
"version": "4.3.1",
44
"description": "UI components for React and Blueprint.js",
55
"main": "dist/cjs/index.js",
66
"module": "dist/esm/index.js",

packages/ui-components/src/infinite-scroll.ts

Lines changed: 80 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @ts-nocheck
22
import h from "@macrostrat/hyper";
33
import update, { Spec } from "immutability-helper";
4-
import React, { useReducer, useEffect, useRef, useCallback, memo } from "react";
4+
import React, { useReducer, useEffect, useRef, useCallback } from "react";
55
import { Spinner, NonIdealState } from "@blueprintjs/core";
66
import { APIParams, QueryParams } from "./util/query-string";
77
import { 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-
4723
type UpdateState<T> = { type: "update-state"; spec: Spec<ScrollState<T>> };
48-
type LoadNextPage = {
49-
type: "load-next-page";
50-
page: number;
51-
};
5224
type 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>;
6132
type 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

172145
function 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

326301
InfiniteScrollView.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

Comments
 (0)