@shopify/flash-list 2.0.0-alpha.15 → 2.0.0-alpha.16

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 (35) hide show
  1. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  2. package/dist/recyclerview/RecyclerView.js +7 -1
  3. package/dist/recyclerview/RecyclerView.js.map +1 -1
  4. package/dist/recyclerview/hooks/useLayoutState.d.ts +3 -1
  5. package/dist/recyclerview/hooks/useLayoutState.d.ts.map +1 -1
  6. package/dist/recyclerview/hooks/useLayoutState.js +5 -3
  7. package/dist/recyclerview/hooks/useLayoutState.js.map +1 -1
  8. package/dist/recyclerview/hooks/useRecyclingState.d.ts +4 -2
  9. package/dist/recyclerview/hooks/useRecyclingState.d.ts.map +1 -1
  10. package/dist/recyclerview/hooks/useRecyclingState.js +2 -2
  11. package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -1
  12. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +9 -1
  13. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  14. package/dist/recyclerview/layout-managers/GridLayoutManager.js +22 -7
  15. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  16. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +26 -6
  17. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
  18. package/dist/recyclerview/layout-managers/LayoutManager.js +69 -12
  19. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
  20. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts +9 -1
  21. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -1
  22. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +28 -12
  23. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
  24. package/dist/tsconfig.tsbuildinfo +1 -1
  25. package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
  26. package/dist/viewability/ViewabilityManager.js +10 -3
  27. package/dist/viewability/ViewabilityManager.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/recyclerview/RecyclerView.tsx +13 -6
  30. package/src/recyclerview/hooks/useLayoutState.ts +15 -6
  31. package/src/recyclerview/hooks/useRecyclingState.ts +11 -7
  32. package/src/recyclerview/layout-managers/GridLayoutManager.ts +26 -6
  33. package/src/recyclerview/layout-managers/LayoutManager.ts +74 -15
  34. package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +30 -8
  35. package/src/viewability/ViewabilityManager.ts +10 -6
@@ -1,6 +1,10 @@
1
- import { Dispatch, SetStateAction, useCallback, useMemo, useRef } from "react";
1
+ import { useCallback, useMemo, useRef } from "react";
2
2
 
3
- import { useLayoutState } from "./useLayoutState";
3
+ import { LayoutStateSetter, useLayoutState } from "./useLayoutState";
4
+
5
+ export type RecyclingStateSetter<T> = LayoutStateSetter<T>;
6
+
7
+ export type RecyclingStateInitialValue<T> = T | (() => T);
4
8
 
5
9
  /**
6
10
  * A custom hook that provides state management with automatic reset functionality.
@@ -16,10 +20,10 @@ import { useLayoutState } from "./useLayoutState";
16
20
  * - A setState function that works like useState's setState
17
21
  */
18
22
  export function useRecyclingState<T>(
19
- initialState: T | (() => T),
23
+ initialState: RecyclingStateInitialValue<T>,
20
24
  deps: React.DependencyList,
21
25
  onReset?: () => void
22
- ): [T, Dispatch<SetStateAction<T>>] {
26
+ ): [T, RecyclingStateSetter<T>] {
23
27
  // Store the current state value in a ref to persist between renders
24
28
  const valueStore = useRef<T>();
25
29
  // Use layoutState to trigger re-renders when state changes
@@ -42,8 +46,8 @@ export function useRecyclingState<T>(
42
46
  * Proxy setState function that updates the stored value and triggers a re-render.
43
47
  * Only triggers a re-render if the new value is different from the current value.
44
48
  */
45
- const setStateProxy = useCallback(
46
- (newValue: T | ((prevValue: T) => T)) => {
49
+ const setStateProxy: RecyclingStateSetter<T> = useCallback(
50
+ (newValue, skipParentLayout) => {
47
51
  // Calculate next state value from function or direct value
48
52
  const nextState =
49
53
  typeof newValue === "function"
@@ -53,7 +57,7 @@ export function useRecyclingState<T>(
53
57
  // Only update and trigger re-render if value has changed
54
58
  if (nextState !== valueStore.current) {
55
59
  valueStore.current = nextState;
56
- setCounter((prev) => prev + 1);
60
+ setCounter((prev) => prev + 1, skipParentLayout);
57
61
  }
58
62
  },
59
63
  [setCounter]
@@ -14,6 +14,9 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
14
14
  /** The width of the bounded area for the grid */
15
15
  private boundedSize: number;
16
16
 
17
+ /** If there's a span change for grid layout, we need to recompute all the widths */
18
+ private fullRelayoutRequired = false;
19
+
17
20
  constructor(params: LayoutParams, previousLayoutManager?: RVLayoutManager) {
18
21
  super(params, previousLayoutManager);
19
22
  this.boundedSize = params.windowSize.width;
@@ -33,10 +36,7 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
33
36
  this.boundedSize = params.windowSize.width;
34
37
  if (this.layouts.length > 0) {
35
38
  // update all widths
36
- for (let i = 0; i < this.layouts.length; i++) {
37
- this.layouts[i].width = this.getWidth(i);
38
- }
39
- // console.log("-----> recomputeLayouts");
39
+ this.updateAllWidths();
40
40
 
41
41
  this.recomputeLayouts(0, this.layouts.length - 1);
42
42
  this.requiresRepaint = true;
@@ -57,6 +57,13 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
57
57
  layout.isHeightMeasured = true;
58
58
  layout.isWidthMeasured = true;
59
59
  }
60
+
61
+ // TODO: Can be optimized
62
+ if (this.fullRelayoutRequired) {
63
+ this.updateAllWidths();
64
+ this.fullRelayoutRequired = false;
65
+ return 0;
66
+ }
60
67
  }
61
68
 
62
69
  /**
@@ -72,6 +79,14 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
72
79
  layout.enforcedWidth = true;
73
80
  }
74
81
 
82
+ /**
83
+ * Handles span change for an item.
84
+ * @param index Index of the item
85
+ */
86
+ handleSpanChange(index: number) {
87
+ this.fullRelayoutRequired = true;
88
+ }
89
+
75
90
  /**
76
91
  * Returns the total size of the layout area.
77
92
  * @returns RVDimension containing width and height of the layout
@@ -122,8 +137,7 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
122
137
  * @returns Width of the item
123
138
  */
124
139
  private getWidth(index: number): number {
125
- const span = this.getSpanSizeInfo(index).span ?? 1;
126
- return (this.boundedSize / this.maxColumns) * span;
140
+ return (this.boundedSize / this.maxColumns) * this.getSpan(index);
127
141
  }
128
142
 
129
143
  /**
@@ -205,6 +219,12 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
205
219
  return y + maxHeight;
206
220
  }
207
221
 
222
+ private updateAllWidths() {
223
+ for (let i = 0; i < this.layouts.length; i++) {
224
+ this.layouts[i].width = this.getWidth(i);
225
+ }
226
+ }
227
+
208
228
  /**
209
229
  * Checks if an item can fit within the bounded width.
210
230
  * @param itemX Starting X position of the item
@@ -20,8 +20,6 @@ export abstract class RVLayoutManager {
20
20
  protected layouts: RVLayout[];
21
21
  /** Dimensions of the visible window/viewport */
22
22
  protected windowSize: RVDimension;
23
- /** Information about item spans and sizes */
24
- protected spanSizeInfo: SpanSizeInfo = {};
25
23
  /** Maximum number of columns in the layout */
26
24
  protected maxColumns: number;
27
25
 
@@ -41,6 +39,13 @@ export abstract class RVLayoutManager {
41
39
  private widthAverageWindow: MultiTypeAverageWindow;
42
40
  /** Maximum number of items to process in a single layout pass */
43
41
  private maxItemsToProcess = 250; // TODO: make this dynamic
42
+ /** Information about item spans and sizes */
43
+ private spanSizeInfo: SpanSizeInfo = {};
44
+ /** Span tracker for each item */
45
+ private spanTracker: (number | undefined)[] = [];
46
+
47
+ /** Current max index with changed layout */
48
+ private currentMaxIndexWithChangedLayout = -1;
44
49
 
45
50
  constructor(params: LayoutParams, previousLayoutManager?: RVLayoutManager) {
46
51
  this.heightAverageWindow = new MultiTypeAverageWindow(5, 200);
@@ -164,37 +169,46 @@ export abstract class RVLayoutManager {
164
169
 
165
170
  if (this.layouts.length > totalItemCount) {
166
171
  this.layouts.length = totalItemCount;
172
+ this.spanTracker.length = totalItemCount;
167
173
  minRecomputeIndex = totalItemCount - 1; // <0 gets skipped so it's safe to set to totalItemCount - 1
168
174
  }
169
175
  // update average windows
170
176
  minRecomputeIndex = Math.min(
171
177
  minRecomputeIndex,
172
- this.computeEstimatesAndMinRecomputeIndex(layoutInfo)
178
+ this.computeEstimatesAndMinMaxChangedLayout(layoutInfo)
173
179
  );
174
180
 
175
181
  if (this.layouts.length < totalItemCount && totalItemCount > 0) {
176
182
  const startIndex = this.layouts.length;
177
183
  this.layouts.length = totalItemCount;
184
+ this.spanTracker.length = totalItemCount;
178
185
  for (let i = startIndex; i < totalItemCount; i++) {
179
186
  this.getLayout(i);
187
+ this.getSpan(i);
180
188
  }
181
189
  this.recomputeLayouts(startIndex, totalItemCount - 1);
182
190
  }
183
- minRecomputeIndex = Math.min(
184
- minRecomputeIndex,
185
- this.processLayoutInfo(layoutInfo, totalItemCount) ?? minRecomputeIndex
186
- );
191
+
187
192
  // compute minRecomputeIndex
193
+
188
194
  minRecomputeIndex = Math.min(
189
195
  minRecomputeIndex,
190
- this.computeEstimatesAndMinRecomputeIndex(layoutInfo)
196
+ this.computeMinIndexWithChangedSpan(layoutInfo),
197
+ this.processLayoutInfo(layoutInfo, totalItemCount) ?? minRecomputeIndex,
198
+ this.computeEstimatesAndMinMaxChangedLayout(layoutInfo)
191
199
  );
200
+
192
201
  if (minRecomputeIndex >= 0 && minRecomputeIndex < totalItemCount) {
202
+ const maxRecomputeIndex = this.getMaxRecomputeIndex(minRecomputeIndex);
193
203
  this.recomputeLayouts(
194
204
  this.getMinRecomputeIndex(minRecomputeIndex),
195
- this.getMaxRecomputeIndex(minRecomputeIndex)
205
+ maxRecomputeIndex
196
206
  );
207
+ if (maxRecomputeIndex + 1 < totalItemCount) {
208
+ this.layouts[maxRecomputeIndex + 1].repositionPending = true;
209
+ }
197
210
  }
211
+ this.currentMaxIndexWithChangedLayout = -1;
198
212
  }
199
213
 
200
214
  /**
@@ -260,16 +274,30 @@ export abstract class RVLayoutManager {
260
274
  protected abstract estimateLayout(index: number): void;
261
275
 
262
276
  /**
263
- * Gets span size information for an item, applying any overrides.
277
+ * Gets span for an item, applying any overrides.
278
+ * This is intended to be called during a relayout call. The value is tracked and used to determine if a span change has occurred.
279
+ * If skipTracking is true, the operation is not tracked. Can be useful if span is required outside of a relayout call.
280
+ * The tracker is used to call handleSpanChange if a span change has occurred before relayout call.
281
+ * // TODO: improve this contract.
264
282
  * @param index Index of the item
265
- * @returns SpanSizeInfo for the item
283
+ * @returns Span for the item
266
284
  */
267
- protected getSpanSizeInfo(index: number): SpanSizeInfo {
285
+ protected getSpan(index: number, skipTracking = false): number {
268
286
  this.spanSizeInfo.span = undefined;
269
287
  this.overrideItemLayout(index, this.spanSizeInfo);
270
- return this.spanSizeInfo;
288
+ const span = Math.min(this.spanSizeInfo.span ?? 1, this.maxColumns);
289
+ if (!skipTracking) {
290
+ this.spanTracker[index] = span;
291
+ }
292
+ return span;
271
293
  }
272
294
 
295
+ /**
296
+ * Method to handle span change for an item. Can be overridden by subclasses.
297
+ * @param index Index of the item
298
+ */
299
+ protected handleSpanChange(index: number) {}
300
+
273
301
  /**
274
302
  * Gets the maximum index to process in a single layout pass.
275
303
  * @param startIndex Starting index
@@ -277,7 +305,8 @@ export abstract class RVLayoutManager {
277
305
  */
278
306
  private getMaxRecomputeIndex(startIndex: number): number {
279
307
  return Math.min(
280
- startIndex + this.maxItemsToProcess,
308
+ Math.max(startIndex, this.currentMaxIndexWithChangedLayout) +
309
+ this.maxItemsToProcess,
281
310
  this.layouts.length - 1
282
311
  );
283
312
  }
@@ -296,7 +325,7 @@ export abstract class RVLayoutManager {
296
325
  * @param layoutInfo Array of layout information for items
297
326
  * @returns Minimum index that needs recomputation
298
327
  */
299
- private computeEstimatesAndMinRecomputeIndex(
328
+ private computeEstimatesAndMinMaxChangedLayout(
300
329
  layoutInfo: RVLayoutInfo[]
301
330
  ): number {
302
331
  let minRecomputeIndex = Number.MAX_VALUE;
@@ -307,10 +336,18 @@ export abstract class RVLayoutManager {
307
336
  !storedLayout ||
308
337
  !storedLayout.isHeightMeasured ||
309
338
  !storedLayout.isWidthMeasured ||
339
+ storedLayout.repositionPending ||
310
340
  areDimensionsNotEqual(storedLayout.height, dimensions.height) ||
311
341
  areDimensionsNotEqual(storedLayout.width, dimensions.width)
312
342
  ) {
313
343
  minRecomputeIndex = Math.min(minRecomputeIndex, index);
344
+ this.currentMaxIndexWithChangedLayout = Math.max(
345
+ this.currentMaxIndexWithChangedLayout,
346
+ index
347
+ );
348
+ if (storedLayout?.repositionPending) {
349
+ storedLayout.repositionPending = false;
350
+ }
314
351
  }
315
352
  this.heightAverageWindow.addValue(
316
353
  dimensions.height,
@@ -323,6 +360,21 @@ export abstract class RVLayoutManager {
323
360
  }
324
361
  return minRecomputeIndex;
325
362
  }
363
+
364
+ private computeMinIndexWithChangedSpan(layoutInfo: RVLayoutInfo[]): number {
365
+ let minIndexWithChangedSpan = Number.MAX_VALUE;
366
+ for (const info of layoutInfo) {
367
+ const { index } = info;
368
+ const span = this.getSpan(index, true);
369
+ const storedSpan = this.spanTracker[index];
370
+ if (span !== storedSpan) {
371
+ this.spanTracker[index] = span;
372
+ this.handleSpanChange(index);
373
+ minIndexWithChangedSpan = Math.min(minIndexWithChangedSpan, index);
374
+ }
375
+ }
376
+ return minIndexWithChangedSpan;
377
+ }
326
378
  }
327
379
 
328
380
  /**
@@ -467,6 +519,13 @@ export interface RVLayout extends RVDimension {
467
519
  * When false, the height is determined by content
468
520
  */
469
521
  enforcedHeight?: boolean;
522
+
523
+ /**
524
+ * When true, the layout is pending repositioning
525
+ * When false, the layout is up to date
526
+ * ViewHolder update is not required.
527
+ */
528
+ repositionPending?: boolean;
470
529
  }
471
530
 
472
531
  /**
@@ -19,10 +19,13 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
19
19
  /** Current column index for sequential placement */
20
20
  private currentColumn = 0;
21
21
 
22
+ /** If there's a span change for masonry layout, we need to recompute all the widths */
23
+ private fullRelayoutRequired = false;
24
+
22
25
  constructor(params: LayoutParams, previousLayoutManager?: RVLayoutManager) {
23
26
  super(params, previousLayoutManager);
24
27
  this.boundedSize = params.windowSize.width;
25
- this.optimizeItemArrangement = params.optimizeItemArrangement ?? false;
28
+ this.optimizeItemArrangement = params.optimizeItemArrangement;
26
29
  this.columnHeights = this.columnHeights ?? Array(this.maxColumns).fill(0);
27
30
  }
28
31
 
@@ -44,10 +47,7 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
44
47
  // console.log("-----> recomputeLayouts");
45
48
 
46
49
  // update all widths
47
- for (let i = 0; i < this.layouts.length; i++) {
48
- this.layouts[i].width = this.getWidth(i);
49
- this.layouts[i].minHeight = undefined;
50
- }
50
+ this.updateAllWidths();
51
51
  this.recomputeLayouts(0, this.layouts.length - 1);
52
52
  this.requiresRepaint = true;
53
53
  }
@@ -69,6 +69,13 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
69
69
  layout.isWidthMeasured = true;
70
70
  this.layouts[index] = layout;
71
71
  }
72
+
73
+ // TODO: Can be optimized
74
+ if (this.fullRelayoutRequired) {
75
+ this.updateAllWidths();
76
+ this.fullRelayoutRequired = false;
77
+ return 0;
78
+ }
72
79
  }
73
80
 
74
81
  /**
@@ -87,6 +94,14 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
87
94
  layout.enforcedWidth = true;
88
95
  }
89
96
 
97
+ /**
98
+ * Handles span change for an item.
99
+ * @param index Index of the item
100
+ */
101
+ handleSpanChange(index: number) {
102
+ this.fullRelayoutRequired = true;
103
+ }
104
+
90
105
  /**
91
106
  * Returns the total size of the layout area.
92
107
  * @returns RVDimension containing width and height of the layout
@@ -124,7 +139,8 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
124
139
 
125
140
  for (let i = startIndex; i < itemCount; i++) {
126
141
  const layout = this.getLayout(i);
127
- const span = this.getSpanSizeInfo(i).span ?? 1;
142
+ // Skip tracking span because we're not changing widths
143
+ const span = this.getSpan(i, true);
128
144
 
129
145
  if (this.optimizeItemArrangement) {
130
146
  if (span === 1) {
@@ -147,8 +163,14 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
147
163
  * @returns Width of the item
148
164
  */
149
165
  private getWidth(index: number): number {
150
- const span = this.getSpanSizeInfo(index).span ?? 1;
151
- return (this.boundedSize / this.maxColumns) * span;
166
+ return (this.boundedSize / this.maxColumns) * this.getSpan(index);
167
+ }
168
+
169
+ private updateAllWidths() {
170
+ for (let i = 0; i < this.layouts.length; i++) {
171
+ this.layouts[i].width = this.getWidth(i);
172
+ this.layouts[i].minHeight = undefined;
173
+ }
152
174
  }
153
175
 
154
176
  /**
@@ -22,17 +22,21 @@ export default class ViewabilityManager<T> {
22
22
  this.viewabilityHelpers.push(
23
23
  this.createViewabilityHelper(
24
24
  flashListRef.props.viewabilityConfig,
25
- flashListRef.props.onViewableItemsChanged
25
+ (info) => {
26
+ flashListRef.props.onViewableItemsChanged?.(info);
27
+ }
26
28
  )
27
29
  );
28
30
  }
29
31
  (flashListRef.props.viewabilityConfigCallbackPairs ?? []).forEach(
30
- (pair) => {
32
+ (pair, index) => {
31
33
  this.viewabilityHelpers.push(
32
- this.createViewabilityHelper(
33
- pair.viewabilityConfig,
34
- pair.onViewableItemsChanged
35
- )
34
+ this.createViewabilityHelper(pair.viewabilityConfig, (info) => {
35
+ const callback =
36
+ flashListRef.props.viewabilityConfigCallbackPairs?.[index]
37
+ ?.onViewableItemsChanged;
38
+ callback?.(info);
39
+ })
36
40
  );
37
41
  }
38
42
  );