@shopify/flash-list 2.0.0-alpha.20 → 2.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FlashListProps.d.ts +1 -1
- package/dist/__tests__/LayoutCommitObserver.test.d.ts +2 -0
- package/dist/__tests__/LayoutCommitObserver.test.d.ts.map +1 -0
- package/dist/__tests__/LayoutCommitObserver.test.js +35 -0
- package/dist/__tests__/LayoutCommitObserver.test.js.map +1 -0
- package/dist/benchmark/useBenchmark.js +0 -25
- package/dist/benchmark/useBenchmark.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/recyclerview/LayoutCommitObserver.d.ts +12 -0
- package/dist/recyclerview/LayoutCommitObserver.d.ts.map +1 -0
- package/dist/recyclerview/LayoutCommitObserver.js +62 -0
- package/dist/recyclerview/LayoutCommitObserver.js.map +1 -0
- package/dist/recyclerview/RecyclerViewManager.d.ts +4 -1
- package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerViewManager.js +34 -26
- package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewController.js +12 -9
- package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/FlashListProps.ts +1 -1
- package/src/__tests__/LayoutCommitObserver.test.tsx +60 -0
- package/src/benchmark/useBenchmark.ts +0 -37
- package/src/index.ts +4 -0
- package/src/recyclerview/LayoutCommitObserver.tsx +74 -0
- package/src/recyclerview/RecyclerViewManager.ts +31 -22
- package/src/recyclerview/hooks/useRecyclerViewController.tsx +15 -14
|
@@ -199,42 +199,5 @@ function computeSuggestions(
|
|
|
199
199
|
`Data count is low. Try to increase it to a large number (e.g 200) using the 'useDataMultiplier' hook.`
|
|
200
200
|
);
|
|
201
201
|
}
|
|
202
|
-
const distanceFromWindow = roundToDecimalPlaces(
|
|
203
|
-
flashListRef.current.firstItemOffset,
|
|
204
|
-
0
|
|
205
|
-
);
|
|
206
|
-
if (
|
|
207
|
-
(flashListRef.current.props.estimatedFirstItemOffset || 0) !==
|
|
208
|
-
distanceFromWindow
|
|
209
|
-
) {
|
|
210
|
-
suggestions.push(
|
|
211
|
-
`estimatedFirstItemOffset can be set to ${distanceFromWindow}`
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
const rlv = flashListRef.current.recyclerlistview_unsafe;
|
|
215
|
-
const horizontal = flashListRef.current.props.horizontal;
|
|
216
|
-
if (rlv) {
|
|
217
|
-
const sizeArray = rlv.props.dataProvider
|
|
218
|
-
.getAllData()
|
|
219
|
-
.map((_, index) =>
|
|
220
|
-
horizontal
|
|
221
|
-
? rlv.getLayout?.(index)?.width || 0
|
|
222
|
-
: rlv.getLayout?.(index)?.height || 0
|
|
223
|
-
);
|
|
224
|
-
const averageSize = Math.round(
|
|
225
|
-
sizeArray.reduce((prev, current) => prev + current, 0) /
|
|
226
|
-
sizeArray.length
|
|
227
|
-
);
|
|
228
|
-
if (
|
|
229
|
-
Math.abs(
|
|
230
|
-
averageSize -
|
|
231
|
-
(flashListRef.current.props.estimatedItemSize ??
|
|
232
|
-
flashListRef.current.state.layoutProvider
|
|
233
|
-
.defaultEstimatedItemSize)
|
|
234
|
-
) > 5
|
|
235
|
-
) {
|
|
236
|
-
suggestions.push(`estimatedItemSize can be set to ${averageSize}`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
202
|
}
|
|
240
203
|
}
|
package/src/index.ts
CHANGED
|
@@ -54,6 +54,10 @@ export { default as CellContainer } from "./native/cell-container/CellContainer"
|
|
|
54
54
|
export { RecyclerView } from "./recyclerview/RecyclerView";
|
|
55
55
|
export { RecyclerViewProps } from "./recyclerview/RecyclerViewProps";
|
|
56
56
|
export { useFlashListContext } from "./recyclerview/RecyclerViewContextProvider";
|
|
57
|
+
export {
|
|
58
|
+
LayoutCommitObserver,
|
|
59
|
+
LayoutCommitObserverProps,
|
|
60
|
+
} from "./recyclerview/LayoutCommitObserver";
|
|
57
61
|
|
|
58
62
|
// @ts-ignore - This is ignored by TypeScript but will be present in the compiled JS
|
|
59
63
|
// In the compiled JS, this will override the previous FlashList export with a conditional one
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React, { useLayoutEffect, useMemo, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
RecyclerViewContext,
|
|
5
|
+
RecyclerViewContextProvider,
|
|
6
|
+
useRecyclerViewContext,
|
|
7
|
+
} from "./RecyclerViewContextProvider";
|
|
8
|
+
import { useLayoutState } from "./hooks/useLayoutState";
|
|
9
|
+
|
|
10
|
+
export interface LayoutCommitObserverProps {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
onCommitLayoutEffect?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* LayoutCommitObserver can be used to observe when FlashList commits a layout.
|
|
17
|
+
* It is useful when your component has one or more FlashLists somewhere down the tree.
|
|
18
|
+
* LayoutCommitObserver will trigger `onCommitLayoutEffect` when all of the FlashLists in the tree have finished their first commit.
|
|
19
|
+
*/
|
|
20
|
+
export const LayoutCommitObserver = React.memo(
|
|
21
|
+
(props: LayoutCommitObserverProps) => {
|
|
22
|
+
const { children, onCommitLayoutEffect } = props;
|
|
23
|
+
const parentRecyclerViewContext = useRecyclerViewContext();
|
|
24
|
+
const [_, setRenderId] = useLayoutState(0);
|
|
25
|
+
const pendingChildIds = useRef<Set<string>>(new Set()).current;
|
|
26
|
+
|
|
27
|
+
useLayoutEffect(() => {
|
|
28
|
+
if (pendingChildIds.size > 0) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
onCommitLayoutEffect?.();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Create context for child components
|
|
35
|
+
const recyclerViewContext: RecyclerViewContext<unknown> = useMemo(() => {
|
|
36
|
+
return {
|
|
37
|
+
layout: () => {
|
|
38
|
+
setRenderId((prev) => prev + 1);
|
|
39
|
+
},
|
|
40
|
+
getRef: () => {
|
|
41
|
+
return parentRecyclerViewContext?.getRef() ?? null;
|
|
42
|
+
},
|
|
43
|
+
getParentRef: () => {
|
|
44
|
+
return parentRecyclerViewContext?.getParentRef() ?? null;
|
|
45
|
+
},
|
|
46
|
+
getParentScrollViewRef: () => {
|
|
47
|
+
return parentRecyclerViewContext?.getParentScrollViewRef() ?? null;
|
|
48
|
+
},
|
|
49
|
+
getScrollViewRef: () => {
|
|
50
|
+
return parentRecyclerViewContext?.getScrollViewRef() ?? null;
|
|
51
|
+
},
|
|
52
|
+
markChildLayoutAsPending: (id: string) => {
|
|
53
|
+
parentRecyclerViewContext?.markChildLayoutAsPending(id);
|
|
54
|
+
pendingChildIds.add(id);
|
|
55
|
+
},
|
|
56
|
+
unmarkChildLayoutAsPending: (id: string) => {
|
|
57
|
+
parentRecyclerViewContext?.unmarkChildLayoutAsPending(id);
|
|
58
|
+
if (pendingChildIds.has(id)) {
|
|
59
|
+
pendingChildIds.delete(id);
|
|
60
|
+
recyclerViewContext.layout();
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}, [parentRecyclerViewContext, pendingChildIds, setRenderId]);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<RecyclerViewContextProvider value={recyclerViewContext}>
|
|
68
|
+
{children}
|
|
69
|
+
</RecyclerViewContextProvider>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
LayoutCommitObserver.displayName = "LayoutCommitObserver";
|
|
@@ -20,13 +20,14 @@ import {
|
|
|
20
20
|
import { RenderStackManager } from "./RenderStackManager";
|
|
21
21
|
// Abstracts layout manager, render stack manager and viewability manager and generates render stack (progressively on load)
|
|
22
22
|
export class RecyclerViewManager<T> {
|
|
23
|
-
private initialDrawBatchSize =
|
|
23
|
+
private initialDrawBatchSize = 2;
|
|
24
24
|
private engagedIndicesTracker: RVEngagedIndicesTracker;
|
|
25
25
|
private renderStackManager: RenderStackManager;
|
|
26
26
|
private layoutManager?: RVLayoutManager;
|
|
27
27
|
// Map of index to key
|
|
28
28
|
private isFirstLayoutComplete = false;
|
|
29
29
|
private hasRenderedProgressively = false;
|
|
30
|
+
private progressiveRenderCount = 0;
|
|
30
31
|
private propsRef: RecyclerViewProps<T>;
|
|
31
32
|
private itemViewabilityManager: ViewabilityManager<T>;
|
|
32
33
|
private _isDisposed = false;
|
|
@@ -53,8 +54,12 @@ export class RecyclerViewManager<T> {
|
|
|
53
54
|
return this._isDisposed;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
public get numColumns() {
|
|
58
|
+
return this.propsRef.numColumns ?? 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
constructor(props: RecyclerViewProps<T>) {
|
|
57
|
-
this.
|
|
62
|
+
this.getDataKey = this.getDataKey.bind(this);
|
|
58
63
|
this.getItemType = this.getItemType.bind(this);
|
|
59
64
|
this.overrideItemLayout = this.overrideItemLayout.bind(this);
|
|
60
65
|
this.propsRef = props;
|
|
@@ -68,7 +73,7 @@ export class RecyclerViewManager<T> {
|
|
|
68
73
|
// updates render stack based on the engaged indices which are sorted. Recycles unused keys.
|
|
69
74
|
private updateRenderStack = (engagedIndices: ConsecutiveNumbers): void => {
|
|
70
75
|
this.renderStackManager.sync(
|
|
71
|
-
this.
|
|
76
|
+
this.getDataKey,
|
|
72
77
|
this.getItemType,
|
|
73
78
|
engagedIndices,
|
|
74
79
|
this.getDataLength()
|
|
@@ -87,11 +92,6 @@ export class RecyclerViewManager<T> {
|
|
|
87
92
|
this.propsRef = props;
|
|
88
93
|
this.engagedIndicesTracker.drawDistance =
|
|
89
94
|
props.drawDistance ?? this.engagedIndicesTracker.drawDistance;
|
|
90
|
-
if (this.propsRef.drawDistance === 0) {
|
|
91
|
-
this.initialDrawBatchSize = 1;
|
|
92
|
-
} else {
|
|
93
|
-
this.initialDrawBatchSize = (props.numColumns ?? 1) * 2;
|
|
94
|
-
}
|
|
95
95
|
this.initialDrawBatchSize =
|
|
96
96
|
this.propsRef.overrideProps?.initialDrawBatchSize ??
|
|
97
97
|
this.initialDrawBatchSize;
|
|
@@ -221,7 +221,7 @@ export class RecyclerViewManager<T> {
|
|
|
221
221
|
}
|
|
222
222
|
const layoutManagerParams: LayoutParams = {
|
|
223
223
|
windowSize,
|
|
224
|
-
maxColumns: this.
|
|
224
|
+
maxColumns: this.numColumns,
|
|
225
225
|
horizontal: Boolean(this.propsRef.horizontal),
|
|
226
226
|
optimizeItemArrangement: this.propsRef.optimizeItemArrangement ?? true,
|
|
227
227
|
overrideItemLayout: this.overrideItemLayout,
|
|
@@ -298,7 +298,7 @@ export class RecyclerViewManager<T> {
|
|
|
298
298
|
processDataUpdate() {
|
|
299
299
|
if (this.hasLayout()) {
|
|
300
300
|
this.modifyChildrenLayout([], this.propsRef.data?.length ?? 0);
|
|
301
|
-
if (!this.recomputeEngagedIndices()) {
|
|
301
|
+
if (this.hasRenderedProgressively && !this.recomputeEngagedIndices()) {
|
|
302
302
|
// recomputeEngagedIndices will update the render stack if there are any changes in the engaged indices.
|
|
303
303
|
// It's important to update render stack so that elements are assgined right keys incase items were deleted.
|
|
304
304
|
this.updateRenderStack(this.engagedIndicesTracker.getEngagedIndices());
|
|
@@ -346,17 +346,28 @@ export class RecyclerViewManager<T> {
|
|
|
346
346
|
return this.propsRef.data?.length ?? 0;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
hasStableDataKeys() {
|
|
350
|
+
return Boolean(this.propsRef.keyExtractor);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
getDataKey(index: number): string {
|
|
354
|
+
return (
|
|
355
|
+
this.propsRef.keyExtractor?.(this.propsRef.data![index], index) ??
|
|
356
|
+
index.toString()
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
349
360
|
private getLayoutManagerClass() {
|
|
350
361
|
// throw errors for incompatible props
|
|
351
362
|
if (this.propsRef.masonry && this.propsRef.horizontal) {
|
|
352
363
|
throw new Error("Masonry and horizontal props are incompatible");
|
|
353
364
|
}
|
|
354
|
-
if (
|
|
365
|
+
if (this.numColumns > 1 && this.propsRef.horizontal) {
|
|
355
366
|
throw new Error("numColumns and horizontal props are incompatible");
|
|
356
367
|
}
|
|
357
368
|
return this.propsRef.masonry
|
|
358
369
|
? RVMasonryLayoutManagerImpl
|
|
359
|
-
:
|
|
370
|
+
: this.numColumns > 1 && !this.propsRef.horizontal
|
|
360
371
|
? RVGridLayoutManagerImpl
|
|
361
372
|
: RVLinearLayoutManagerImpl;
|
|
362
373
|
}
|
|
@@ -392,6 +403,7 @@ export class RecyclerViewManager<T> {
|
|
|
392
403
|
}
|
|
393
404
|
|
|
394
405
|
private renderProgressively() {
|
|
406
|
+
this.progressiveRenderCount++;
|
|
395
407
|
const layoutManager = this.layoutManager;
|
|
396
408
|
if (layoutManager) {
|
|
397
409
|
this.applyInitialScrollAdjustment();
|
|
@@ -407,16 +419,20 @@ export class RecyclerViewManager<T> {
|
|
|
407
419
|
this.isFirstLayoutComplete = true;
|
|
408
420
|
}
|
|
409
421
|
|
|
422
|
+
const batchSize =
|
|
423
|
+
this.numColumns *
|
|
424
|
+
this.initialDrawBatchSize ** Math.ceil(this.progressiveRenderCount / 5);
|
|
425
|
+
|
|
410
426
|
// If everything is measured then render stack will be in sync. The buffer items will get rendered in the next update
|
|
411
427
|
// triggered by the useOnLoad hook.
|
|
412
428
|
!this.hasRenderedProgressively &&
|
|
413
429
|
this.updateRenderStack(
|
|
414
|
-
// pick first n indices from visible ones
|
|
430
|
+
// pick first n indices from visible ones based on batch size
|
|
415
431
|
visibleIndices.slice(
|
|
416
432
|
0,
|
|
417
433
|
Math.min(
|
|
418
434
|
visibleIndices.length,
|
|
419
|
-
this.getRenderStack().size +
|
|
435
|
+
this.getRenderStack().size + batchSize
|
|
420
436
|
)
|
|
421
437
|
)
|
|
422
438
|
);
|
|
@@ -430,19 +446,12 @@ export class RecyclerViewManager<T> {
|
|
|
430
446
|
).toString();
|
|
431
447
|
}
|
|
432
448
|
|
|
433
|
-
private getStableId(index: number): string {
|
|
434
|
-
return (
|
|
435
|
-
this.propsRef.keyExtractor?.(this.propsRef.data![index], index) ??
|
|
436
|
-
index.toString()
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
449
|
private overrideItemLayout(index: number, layout: SpanSizeInfo) {
|
|
441
450
|
this.propsRef?.overrideItemLayout?.(
|
|
442
451
|
layout,
|
|
443
452
|
this.propsRef.data![index],
|
|
444
453
|
index,
|
|
445
|
-
this.
|
|
454
|
+
this.numColumns,
|
|
446
455
|
this.propsRef.extraData
|
|
447
456
|
);
|
|
448
457
|
}
|
|
@@ -90,10 +90,9 @@ export function useRecyclerViewController<T>(
|
|
|
90
90
|
);
|
|
91
91
|
|
|
92
92
|
const computeFirstVisibleIndexForOffsetCorrection = useCallback(() => {
|
|
93
|
-
const { data, keyExtractor } = recyclerViewManager.props;
|
|
94
93
|
if (
|
|
95
94
|
recyclerViewManager.getIsFirstLayoutComplete() &&
|
|
96
|
-
|
|
95
|
+
recyclerViewManager.hasStableDataKeys() &&
|
|
97
96
|
recyclerViewManager.getDataLength() > 0 &&
|
|
98
97
|
recyclerViewManager.shouldMaintainVisibleContentPosition()
|
|
99
98
|
) {
|
|
@@ -103,10 +102,8 @@ export function useRecyclerViewController<T>(
|
|
|
103
102
|
recyclerViewManager.computeVisibleIndices().startIndex
|
|
104
103
|
);
|
|
105
104
|
if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
|
|
106
|
-
firstVisibleItemKey.current =
|
|
107
|
-
|
|
108
|
-
firstVisibleIndex
|
|
109
|
-
);
|
|
105
|
+
firstVisibleItemKey.current =
|
|
106
|
+
recyclerViewManager.getDataKey(firstVisibleIndex);
|
|
110
107
|
firstVisibleItemLayout.current = {
|
|
111
108
|
...recyclerViewManager.getLayout(firstVisibleIndex),
|
|
112
109
|
};
|
|
@@ -120,7 +117,7 @@ export function useRecyclerViewController<T>(
|
|
|
120
117
|
* the user's current view position when new messages are added.
|
|
121
118
|
*/
|
|
122
119
|
const applyOffsetCorrection = useCallback(() => {
|
|
123
|
-
const { horizontal, data
|
|
120
|
+
const { horizontal, data } = recyclerViewManager.props;
|
|
124
121
|
|
|
125
122
|
// Execute all pending callbacks from previous scroll offset updates
|
|
126
123
|
// This ensures any scroll operations that were waiting for render are completed
|
|
@@ -132,7 +129,7 @@ export function useRecyclerViewController<T>(
|
|
|
132
129
|
|
|
133
130
|
if (
|
|
134
131
|
recyclerViewManager.getIsFirstLayoutComplete() &&
|
|
135
|
-
|
|
132
|
+
recyclerViewManager.hasStableDataKeys() &&
|
|
136
133
|
currentDataLength > 0 &&
|
|
137
134
|
recyclerViewManager.shouldMaintainVisibleContentPosition()
|
|
138
135
|
) {
|
|
@@ -144,13 +141,14 @@ export function useRecyclerViewController<T>(
|
|
|
144
141
|
.getEngagedIndices()
|
|
145
142
|
.findValue(
|
|
146
143
|
(index) =>
|
|
147
|
-
|
|
144
|
+
recyclerViewManager.getDataKey(index) ===
|
|
148
145
|
firstVisibleItemKey.current
|
|
149
146
|
) ??
|
|
150
147
|
(hasDataChanged
|
|
151
148
|
? data?.findIndex(
|
|
152
149
|
(item, index) =>
|
|
153
|
-
|
|
150
|
+
recyclerViewManager.getDataKey(index) ===
|
|
151
|
+
firstVisibleItemKey.current
|
|
154
152
|
)
|
|
155
153
|
: undefined);
|
|
156
154
|
|
|
@@ -281,10 +279,13 @@ export function useRecyclerViewController<T>(
|
|
|
281
279
|
scrollToEnd: async ({ animated }: ScrollToEdgeParams = {}) => {
|
|
282
280
|
const { data } = recyclerViewManager.props;
|
|
283
281
|
if (data && data.length > 0) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
282
|
+
const lastIndex = data.length - 1;
|
|
283
|
+
if (!recyclerViewManager.getEngagedIndices().includes(lastIndex)) {
|
|
284
|
+
await handlerMethods.scrollToIndex({
|
|
285
|
+
index: lastIndex,
|
|
286
|
+
animated,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
288
289
|
}
|
|
289
290
|
setTimeout(() => {
|
|
290
291
|
scrollViewRef.current!.scrollToEnd({ animated });
|