@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.
Files changed (31) hide show
  1. package/dist/FlashListProps.d.ts +1 -1
  2. package/dist/__tests__/LayoutCommitObserver.test.d.ts +2 -0
  3. package/dist/__tests__/LayoutCommitObserver.test.d.ts.map +1 -0
  4. package/dist/__tests__/LayoutCommitObserver.test.js +35 -0
  5. package/dist/__tests__/LayoutCommitObserver.test.js.map +1 -0
  6. package/dist/benchmark/useBenchmark.js +0 -25
  7. package/dist/benchmark/useBenchmark.js.map +1 -1
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/recyclerview/LayoutCommitObserver.d.ts +12 -0
  13. package/dist/recyclerview/LayoutCommitObserver.d.ts.map +1 -0
  14. package/dist/recyclerview/LayoutCommitObserver.js +62 -0
  15. package/dist/recyclerview/LayoutCommitObserver.js.map +1 -0
  16. package/dist/recyclerview/RecyclerViewManager.d.ts +4 -1
  17. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
  18. package/dist/recyclerview/RecyclerViewManager.js +34 -26
  19. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  20. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  21. package/dist/recyclerview/hooks/useRecyclerViewController.js +12 -9
  22. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  23. package/dist/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +1 -1
  25. package/src/FlashListProps.ts +1 -1
  26. package/src/__tests__/LayoutCommitObserver.test.tsx +60 -0
  27. package/src/benchmark/useBenchmark.ts +0 -37
  28. package/src/index.ts +4 -0
  29. package/src/recyclerview/LayoutCommitObserver.tsx +74 -0
  30. package/src/recyclerview/RecyclerViewManager.ts +31 -22
  31. 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 = 1;
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.getStableId = this.getStableId.bind(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.getStableId,
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.propsRef.numColumns ?? 1,
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 ((this.propsRef.numColumns ?? 1) > 1 && this.propsRef.horizontal) {
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
- : (this.propsRef.numColumns ?? 1) > 1 && !this.propsRef.horizontal
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 and n is size of renderStack
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 + this.initialDrawBatchSize
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.propsRef.numColumns ?? 1,
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
- keyExtractor &&
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 = keyExtractor(
107
- data![firstVisibleIndex],
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, keyExtractor } = recyclerViewManager.props;
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
- keyExtractor &&
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
- keyExtractor?.(data![index], index) ===
144
+ recyclerViewManager.getDataKey(index) ===
148
145
  firstVisibleItemKey.current
149
146
  ) ??
150
147
  (hasDataChanged
151
148
  ? data?.findIndex(
152
149
  (item, index) =>
153
- keyExtractor?.(item, index) === firstVisibleItemKey.current
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
- await handlerMethods.scrollToIndex({
285
- index: data.length - 1,
286
- animated,
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 });