Skip to content

Commit b438b36

Browse files
Abstract batch logic to mock in tests
1 parent 2d8c6e9 commit b438b36

File tree

3 files changed

+83
-31
lines changed

3 files changed

+83
-31
lines changed

jest/setup.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,21 @@ jest.mock(
101101
dispose() {}
102102
},
103103
);
104+
105+
jest.mock(
106+
'@components/InvertedFlatList/BaseInvertedFlatList/RenderTaskQueue',
107+
() =>
108+
class SyncRenderTaskQueue {
109+
private handler: (info: unknown) => void = () => {};
110+
111+
add(info: unknown) {
112+
this.handler(info);
113+
}
114+
115+
setHandler(handler: () => void) {
116+
this.handler = handler;
117+
}
118+
119+
cancel() {}
120+
},
121+
);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
const RENDER_DELAY = 500;
2+
3+
type RenderInfo = {
4+
distanceFromStart: number;
5+
};
6+
7+
class RenderTaskQueue {
8+
private renderInfos: RenderInfo[] = [];
9+
10+
private isRendering = false;
11+
12+
private handler: (info: RenderInfo) => void = () => {};
13+
14+
private timeout: NodeJS.Timeout | null = null;
15+
16+
add(info: RenderInfo) {
17+
this.renderInfos.push(info);
18+
19+
if (!this.isRendering) {
20+
this.render();
21+
}
22+
}
23+
24+
setHandler(handler: (info: RenderInfo) => void) {
25+
this.handler = handler;
26+
}
27+
28+
cancel() {
29+
if (this.timeout == null) {
30+
return;
31+
}
32+
clearTimeout(this.timeout);
33+
}
34+
35+
private render() {
36+
const info = this.renderInfos.shift();
37+
if (!info) {
38+
this.isRendering = false;
39+
return;
40+
}
41+
this.isRendering = true;
42+
43+
this.handler(info);
44+
45+
this.timeout = setTimeout(() => {
46+
this.render();
47+
}, RENDER_DELAY);
48+
}
49+
}
50+
51+
export default RenderTaskQueue;

src/components/InvertedFlatList/BaseInvertedFlatList/index.tsx

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import type {ForwardedRef, MutableRefObject} from 'react';
2-
import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
1+
import type {ForwardedRef} from 'react';
2+
import React, {forwardRef, useCallback, useEffect, useMemo, useState} from 'react';
33
import type {FlatListProps, ListRenderItem, ListRenderItemInfo, FlatList as RNFlatList, ScrollViewProps} from 'react-native';
44
import FlatList from '@components/FlatList';
55
import usePrevious from '@hooks/usePrevious';
66
import getInitialPaginationSize from './getInitialPaginationSize';
7+
import RenderTaskQueue from './RenderTaskQueue';
78

89
// Adapted from https://github.com/facebook/react-native/blob/29a0d7c3b201318a873db0d1b62923f4ce720049/packages/virtualized-lists/Lists/VirtualizeUtils.js#L237
910
function defaultKeyExtractor<T>(item: T | {key: string} | {id: string}, index: number): string {
@@ -26,7 +27,6 @@ type BaseInvertedFlatListProps<T> = Omit<FlatListProps<T>, 'data' | 'renderItem'
2627
};
2728

2829
const AUTOSCROLL_TO_TOP_THRESHOLD = 250;
29-
const RENDER_DELAY = 500;
3030

3131
function BaseInvertedFlatList<T>(props: BaseInvertedFlatListProps<T>, ref: ForwardedRef<RNFlatList>) {
3232
const {shouldEnableAutoScrollToTopThreshold, initialScrollKey, data, onStartReached, renderItem, keyExtractor = defaultKeyExtractor, ...rest} = props;
@@ -55,45 +55,28 @@ function BaseInvertedFlatList<T>(props: BaseInvertedFlatListProps<T>, ref: Forwa
5555
const dataIndexDifference = data.length - displayedData.length;
5656

5757
// Queue up updates to the displayed data to avoid adding too many at once and cause jumps in the list.
58-
const queuedRenders = useRef<Array<{distanceFromStart: number}>>([]);
59-
const isRendering = useRef(false);
60-
61-
const renderTimeout = useRef<NodeJS.Timeout>();
58+
const renderQueue = useMemo(() => new RenderTaskQueue(), []);
6259
useEffect(() => {
6360
return () => {
64-
clearTimeout(renderTimeout.current);
61+
renderQueue.cancel();
6562
};
66-
}, []);
67-
68-
// Use a ref here to make sure we always operate on the latest state.
69-
const updateDisplayedDataRef = useRef() as MutableRefObject<() => void>;
70-
// eslint-disable-next-line react-compiler/react-compiler
71-
updateDisplayedDataRef.current = () => {
72-
const info = queuedRenders.current.shift();
73-
if (!info) {
74-
isRendering.current = false;
75-
return;
76-
}
77-
isRendering.current = true;
63+
}, [renderQueue]);
7864

65+
renderQueue.setHandler((info) => {
7966
if (!isLoadingData) {
8067
onStartReached?.(info);
8168
}
8269
setIsInitialData(false);
8370
const firstDisplayedItem = displayedData.at(0);
8471
setCurrentDataId(firstDisplayedItem ? keyExtractor(firstDisplayedItem, currentDataIndex) : '');
72+
});
8573

86-
renderTimeout.current = setTimeout(() => {
87-
updateDisplayedDataRef.current();
88-
}, RENDER_DELAY);
89-
};
90-
91-
const handleStartReached = useCallback((info: {distanceFromStart: number}) => {
92-
queuedRenders.current.push(info);
93-
if (!isRendering.current) {
94-
updateDisplayedDataRef.current();
95-
}
96-
}, []);
74+
const handleStartReached = useCallback(
75+
(info: {distanceFromStart: number}) => {
76+
renderQueue.add(info);
77+
},
78+
[renderQueue],
79+
);
9780

9881
const handleRenderItem = useCallback(
9982
({item, index, separators}: ListRenderItemInfo<T>) => {

0 commit comments

Comments
 (0)