@shopify/flash-list 2.0.0-alpha.10 → 2.0.0-alpha.11

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 (119) hide show
  1. package/README.md +6 -2
  2. package/dist/AnimatedFlashList.d.ts.map +1 -1
  3. package/dist/AnimatedFlashList.js +3 -3
  4. package/dist/AnimatedFlashList.js.map +1 -1
  5. package/dist/FlashList.d.ts +9 -0
  6. package/dist/FlashList.d.ts.map +1 -1
  7. package/dist/FlashList.js +20 -0
  8. package/dist/FlashList.js.map +1 -1
  9. package/dist/FlashListProps.d.ts +13 -6
  10. package/dist/FlashListProps.d.ts.map +1 -1
  11. package/dist/FlashListProps.js.map +1 -1
  12. package/dist/FlashListRef.d.ts +295 -0
  13. package/dist/FlashListRef.d.ts.map +1 -0
  14. package/dist/FlashListRef.js +3 -0
  15. package/dist/FlashListRef.js.map +1 -0
  16. package/dist/__tests__/RecyclerView.test.js +62 -27
  17. package/dist/__tests__/RecyclerView.test.js.map +1 -1
  18. package/dist/__tests__/RenderStackManager.test.d.ts +2 -0
  19. package/dist/__tests__/RenderStackManager.test.d.ts.map +1 -0
  20. package/dist/__tests__/RenderStackManager.test.js +405 -0
  21. package/dist/__tests__/RenderStackManager.test.js.map +1 -0
  22. package/dist/__tests__/useUnmountAwareCallbacks.test.js +1 -1
  23. package/dist/__tests__/useUnmountAwareCallbacks.test.js.map +1 -1
  24. package/dist/benchmark/useFlatListBenchmark.js +8 -7
  25. package/dist/benchmark/useFlatListBenchmark.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/recyclerview/RecyclerView.d.ts +2 -1
  30. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  31. package/dist/recyclerview/RecyclerView.js +33 -14
  32. package/dist/recyclerview/RecyclerView.js.map +1 -1
  33. package/dist/recyclerview/RecyclerViewContextProvider.d.ts +6 -5
  34. package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -1
  35. package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -1
  36. package/dist/recyclerview/RecyclerViewManager.d.ts +11 -7
  37. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
  38. package/dist/recyclerview/RecyclerViewManager.js +57 -102
  39. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  40. package/dist/recyclerview/RenderStackManager.d.ts +85 -0
  41. package/dist/recyclerview/RenderStackManager.d.ts.map +1 -0
  42. package/dist/recyclerview/RenderStackManager.js +261 -0
  43. package/dist/recyclerview/RenderStackManager.js.map +1 -0
  44. package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
  45. package/dist/recyclerview/ViewHolder.js +5 -3
  46. package/dist/recyclerview/ViewHolder.js.map +1 -1
  47. package/dist/recyclerview/ViewHolderCollection.d.ts +3 -1
  48. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
  49. package/dist/recyclerview/ViewHolderCollection.js +19 -3
  50. package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
  51. package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -1
  52. package/dist/recyclerview/components/ScrollAnchor.js +1 -1
  53. package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
  54. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
  55. package/dist/recyclerview/components/StickyHeaders.js +44 -17
  56. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  57. package/dist/recyclerview/hooks/useBoundDetection.d.ts +1 -2
  58. package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -1
  59. package/dist/recyclerview/hooks/useBoundDetection.js +19 -16
  60. package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
  61. package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -1
  62. package/dist/recyclerview/hooks/useOnLoad.js +4 -6
  63. package/dist/recyclerview/hooks/useOnLoad.js.map +1 -1
  64. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +3 -48
  65. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  66. package/dist/recyclerview/hooks/useRecyclerViewController.js +93 -71
  67. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  68. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
  69. package/dist/recyclerview/hooks/useRecyclerViewManager.js +6 -0
  70. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
  71. package/dist/recyclerview/hooks/useSecondaryProps.js +1 -1
  72. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts +10 -3
  73. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts.map +1 -1
  74. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js +33 -4
  75. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js.map +1 -1
  76. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +6 -0
  77. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  78. package/dist/recyclerview/layout-managers/GridLayoutManager.js +27 -5
  79. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  80. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +2 -2
  81. package/dist/recyclerview/layout-managers/LayoutManager.js +2 -2
  82. package/dist/tsconfig.tsbuildinfo +1 -1
  83. package/jestSetup.js +30 -11
  84. package/package.json +1 -1
  85. package/src/AnimatedFlashList.ts +3 -2
  86. package/src/FlashList.tsx +24 -0
  87. package/src/FlashListProps.ts +16 -7
  88. package/src/FlashListRef.ts +309 -0
  89. package/src/__tests__/RecyclerView.test.tsx +83 -29
  90. package/src/__tests__/RenderStackManager.test.ts +488 -0
  91. package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
  92. package/src/benchmark/useFlatListBenchmark.ts +2 -2
  93. package/src/index.ts +1 -0
  94. package/src/recyclerview/RecyclerView.tsx +38 -23
  95. package/src/recyclerview/RecyclerViewContextProvider.ts +12 -6
  96. package/src/recyclerview/RecyclerViewManager.ts +73 -88
  97. package/src/recyclerview/RenderStackManager.ts +265 -0
  98. package/src/recyclerview/ViewHolder.tsx +5 -3
  99. package/src/recyclerview/ViewHolderCollection.tsx +29 -8
  100. package/src/recyclerview/components/ScrollAnchor.tsx +9 -5
  101. package/src/recyclerview/components/StickyHeaders.tsx +57 -19
  102. package/src/recyclerview/hooks/useBoundDetection.ts +25 -18
  103. package/src/recyclerview/hooks/useOnLoad.ts +4 -6
  104. package/src/recyclerview/hooks/useRecyclerViewController.tsx +104 -125
  105. package/src/recyclerview/hooks/useRecyclerViewManager.ts +6 -0
  106. package/src/recyclerview/hooks/useSecondaryProps.tsx +1 -1
  107. package/src/recyclerview/hooks/useUnmountAwareCallbacks.ts +39 -3
  108. package/src/recyclerview/layout-managers/GridLayoutManager.ts +30 -7
  109. package/src/recyclerview/layout-managers/LayoutManager.ts +2 -2
  110. package/dist/__tests__/RecycleKeyManager.test.d.ts +0 -2
  111. package/dist/__tests__/RecycleKeyManager.test.d.ts.map +0 -1
  112. package/dist/__tests__/RecycleKeyManager.test.js +0 -210
  113. package/dist/__tests__/RecycleKeyManager.test.js.map +0 -1
  114. package/dist/recyclerview/RecycleKeyManager.d.ts +0 -82
  115. package/dist/recyclerview/RecycleKeyManager.d.ts.map +0 -1
  116. package/dist/recyclerview/RecycleKeyManager.js +0 -135
  117. package/dist/recyclerview/RecycleKeyManager.js.map +0 -1
  118. package/src/__tests__/RecycleKeyManager.test.ts +0 -254
  119. package/src/recyclerview/RecycleKeyManager.ts +0 -185
@@ -8,7 +8,13 @@ import {
8
8
  } from "react";
9
9
  import { I18nManager } from "react-native";
10
10
 
11
- import { RecyclerViewProps } from "../RecyclerViewProps";
11
+ import {
12
+ ScrollToOffsetParams,
13
+ ScrollToIndexParams,
14
+ ScrollToItemParams,
15
+ ScrollToEdgeParams,
16
+ FlashListRef,
17
+ } from "../../FlashListRef";
12
18
  import { CompatScroller } from "../components/CompatScroller";
13
19
  import { RecyclerViewManager } from "../RecyclerViewManager";
14
20
  import { adjustOffsetForRTL } from "../utils/adjustOffsetForRTL";
@@ -17,58 +23,7 @@ import { ScrollAnchorRef } from "../components/ScrollAnchor";
17
23
  import { PlatformConfig } from "../../native/config/PlatformHelper";
18
24
 
19
25
  import { useUnmountFlag } from "./useUnmountFlag";
20
- import { useUnmountAwareCallbacks } from "./useUnmountAwareCallbacks";
21
-
22
- /**
23
- * Parameters for scrolling to a specific position in the list.
24
- * Extends ScrollToEdgeParams to include view positioning options.
25
- */
26
- export interface ScrollToParams extends ScrollToEdgeParams {
27
- /** Position of the target item relative to the viewport (0 = top, 0.5 = center, 1 = bottom) */
28
- viewPosition?: number;
29
- /** Additional offset to apply after viewPosition calculation */
30
- viewOffset?: number;
31
- }
32
-
33
- /**
34
- * Parameters for scrolling to a specific offset in the list.
35
- * Used when you want to scroll to an exact pixel position.
36
- */
37
- export interface ScrollToOffsetParams extends ScrollToParams {
38
- /** The pixel offset to scroll to */
39
- offset: number;
40
- /**
41
- * If true, the first item offset will not be added to the offset calculation.
42
- * First offset represents header size or top padding.
43
- */
44
- skipFirstItemOffset?: boolean;
45
- }
46
-
47
- /**
48
- * Parameters for scrolling to a specific index in the list.
49
- * Used when you want to scroll to a specific item by its position in the data array.
50
- */
51
- export interface ScrollToIndexParams extends ScrollToParams {
52
- /** The index of the item to scroll to */
53
- index: number;
54
- }
55
-
56
- /**
57
- * Parameters for scrolling to a specific item in the list.
58
- * Used when you want to scroll to a specific item by its data value.
59
- */
60
- export interface ScrollToItemParams<T> extends ScrollToParams {
61
- /** The item to scroll to */
62
- item: T;
63
- }
64
-
65
- /**
66
- * Base parameters for scrolling to the edges of the list.
67
- */
68
- export interface ScrollToEdgeParams {
69
- /** Whether the scroll should be animated */
70
- animated?: boolean;
71
- }
26
+ import { useUnmountAwareTimeout } from "./useUnmountAwareCallbacks";
72
27
 
73
28
  /**
74
29
  * Comprehensive hook that manages RecyclerView scrolling behavior and provides
@@ -88,61 +43,22 @@ export interface ScrollToEdgeParams {
88
43
  */
89
44
  export function useRecyclerViewController<T>(
90
45
  recyclerViewManager: RecyclerViewManager<T>,
91
- ref: React.Ref<any>,
46
+ ref: React.Ref<FlashListRef<T>>,
92
47
  scrollViewRef: RefObject<CompatScroller>,
93
- scrollAnchorRef: React.RefObject<ScrollAnchorRef>,
94
- props: RecyclerViewProps<T>
48
+ scrollAnchorRef: React.RefObject<ScrollAnchorRef>
95
49
  ) {
96
- const { horizontal, data } = props;
97
50
  const isUnmounted = useUnmountFlag();
98
51
  const [_, setRenderId] = useState(0);
99
52
  const pauseOffsetCorrection = useRef(false);
100
53
  const initialScrollCompletedRef = useRef(false);
101
- const lastDataLengthRef = useRef(data?.length ?? 0);
102
- const { setTimeout } = useUnmountAwareCallbacks();
54
+ const lastDataLengthRef = useRef(recyclerViewManager.props.data?.length ?? 0);
55
+ const { setTimeout } = useUnmountAwareTimeout();
103
56
 
104
57
  // Track the first visible item for maintaining scroll position
105
58
  const firstVisibleItemKey = useRef<string | undefined>(undefined);
106
59
  const firstVisibleItemLayout = useRef<RVLayout | undefined>(undefined);
107
60
  const pendingScrollResolves = useRef<(() => void)[]>([]);
108
61
 
109
- const applyInitialScrollIndex = useCallback(() => {
110
- const initialScrollIndex =
111
- recyclerViewManager.getInitialScrollIndex() ?? -1;
112
- const dataLength = props.data?.length ?? 0;
113
- if (
114
- initialScrollIndex >= 0 &&
115
- initialScrollIndex < dataLength &&
116
- !initialScrollCompletedRef.current &&
117
- recyclerViewManager.getIsFirstLayoutComplete()
118
- ) {
119
- // Use setTimeout to ensure that we keep trying to scroll on first few renders
120
- setTimeout(() => {
121
- initialScrollCompletedRef.current = true;
122
- pauseOffsetCorrection.current = false;
123
- }, 100);
124
-
125
- pauseOffsetCorrection.current = true;
126
-
127
- const offset = horizontal
128
- ? recyclerViewManager.getLayout(initialScrollIndex).x
129
- : recyclerViewManager.getLayout(initialScrollIndex).y;
130
- handlerMethods.scrollToOffset({
131
- offset,
132
- animated: false,
133
- skipFirstItemOffset: false,
134
- });
135
-
136
- setTimeout(() => {
137
- handlerMethods.scrollToOffset({
138
- offset,
139
- animated: false,
140
- skipFirstItemOffset: false,
141
- });
142
- }, 0);
143
- }
144
- }, [recyclerViewManager, props.data]);
145
-
146
62
  // Handle initial scroll position when the list first loads
147
63
  // useOnLoad(recyclerViewManager, () => {
148
64
 
@@ -173,19 +89,21 @@ export function useRecyclerViewController<T>(
173
89
  * the user's current view position when new messages are added.
174
90
  */
175
91
  const applyContentOffset = useCallback(async () => {
92
+ const { horizontal, data, keyExtractor, maintainVisibleContentPosition } =
93
+ recyclerViewManager.props;
176
94
  // Resolve all pending scroll updates from previous calls
177
95
  const resolves = pendingScrollResolves.current;
178
96
  pendingScrollResolves.current = [];
179
97
  resolves.forEach((resolve) => resolve());
180
98
 
181
- const currentDataLength = props.data?.length ?? 0;
99
+ const currentDataLength = data?.length ?? 0;
182
100
 
183
101
  if (
184
- !props.horizontal &&
102
+ !horizontal &&
185
103
  recyclerViewManager.getIsFirstLayoutComplete() &&
186
- props.keyExtractor &&
104
+ keyExtractor &&
187
105
  currentDataLength > 0 &&
188
- props.maintainVisibleContentPosition?.disabled !== true
106
+ maintainVisibleContentPosition?.disabled !== true
189
107
  ) {
190
108
  const hasDataChanged = currentDataLength !== lastDataLengthRef.current;
191
109
  // If we have a tracked first visible item, maintain its position
@@ -195,14 +113,13 @@ export function useRecyclerViewController<T>(
195
113
  .getEngagedIndices()
196
114
  .findValue(
197
115
  (index) =>
198
- props.keyExtractor?.(props.data![index], index) ===
116
+ keyExtractor?.(data![index], index) ===
199
117
  firstVisibleItemKey.current
200
118
  ) ??
201
119
  (hasDataChanged
202
- ? props.data?.findIndex(
120
+ ? data?.findIndex(
203
121
  (item, index) =>
204
- props.keyExtractor?.(item, index) ===
205
- firstVisibleItemKey.current
122
+ keyExtractor?.(item, index) === firstVisibleItemKey.current
206
123
  )
207
124
  : undefined);
208
125
 
@@ -240,11 +157,11 @@ export function useRecyclerViewController<T>(
240
157
  // Update the tracked first visible item
241
158
  const firstVisibleIndex = Math.max(
242
159
  0,
243
- recyclerViewManager.getVisibleIndices().startIndex
160
+ recyclerViewManager.computeVisibleIndices().startIndex
244
161
  );
245
162
  if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
246
- firstVisibleItemKey.current = props.keyExtractor(
247
- props.data![firstVisibleIndex],
163
+ firstVisibleItemKey.current = keyExtractor(
164
+ data![firstVisibleIndex],
248
165
  firstVisibleIndex
249
166
  );
250
167
  firstVisibleItemLayout.current = {
@@ -252,12 +169,20 @@ export function useRecyclerViewController<T>(
252
169
  };
253
170
  }
254
171
  }
255
- lastDataLengthRef.current = props.data?.length ?? 0;
256
- }, [props.data, props.keyExtractor, recyclerViewManager, setTimeout]);
257
-
258
- const handlerMethods = useMemo(() => {
172
+ lastDataLengthRef.current = data?.length ?? 0;
173
+ }, [
174
+ recyclerViewManager,
175
+ scrollAnchorRef,
176
+ scrollViewRef,
177
+ setTimeout,
178
+ updateScrollOffsetAsync,
179
+ ]);
180
+
181
+ const handlerMethods: FlashListRef<T> = useMemo(() => {
259
182
  return {
260
- props,
183
+ get props() {
184
+ return recyclerViewManager.props;
185
+ },
261
186
  /**
262
187
  * Scrolls the list to a specific offset position.
263
188
  * Handles RTL layouts and first item offset adjustments.
@@ -267,6 +192,7 @@ export function useRecyclerViewController<T>(
267
192
  animated,
268
193
  skipFirstItemOffset = true,
269
194
  }: ScrollToOffsetParams) => {
195
+ const { horizontal } = recyclerViewManager.props;
270
196
  if (scrollViewRef.current) {
271
197
  // Adjust offset for RTL layouts in horizontal mode
272
198
  if (I18nManager.isRTL && horizontal) {
@@ -314,6 +240,7 @@ export function useRecyclerViewController<T>(
314
240
  * Scrolls to the end of the list.
315
241
  */
316
242
  scrollToEnd: async ({ animated }: ScrollToEdgeParams = {}) => {
243
+ const { data } = recyclerViewManager.props;
317
244
  if (data && data.length > 0) {
318
245
  await handlerMethods.scrollToIndex({
319
246
  index: data.length - 1,
@@ -345,11 +272,11 @@ export function useRecyclerViewController<T>(
345
272
  viewPosition,
346
273
  viewOffset,
347
274
  }: ScrollToIndexParams) => {
275
+ const { horizontal } = recyclerViewManager.props;
348
276
  if (
349
277
  scrollViewRef.current &&
350
- data &&
351
278
  index >= 0 &&
352
- index < data.length
279
+ index < recyclerViewManager.getDataLength()
353
280
  ) {
354
281
  // Pause the scroll offset adjustments
355
282
  pauseOffsetCorrection.current = true;
@@ -424,6 +351,14 @@ export function useRecyclerViewController<T>(
424
351
  : startScrollOffset +
425
352
  (finalOffset - startScrollOffset) * (i / (steps - 1));
426
353
  await updateScrollOffsetAsync(nextOffset);
354
+
355
+ // In case some change happens in the middle of this operation
356
+ // and the index is out of bounds, scroll to the end
357
+ if (index >= recyclerViewManager.getDataLength()) {
358
+ handlerMethods.scrollToEnd({ animated });
359
+ return;
360
+ }
361
+
427
362
  const newFinalOffset = getFinalOffset();
428
363
  if (
429
364
  (newFinalOffset < initialTargetOffset &&
@@ -480,11 +415,10 @@ export function useRecyclerViewController<T>(
480
415
  viewPosition,
481
416
  viewOffset,
482
417
  }: ScrollToItemParams<T>) => {
418
+ const { data } = recyclerViewManager.props;
483
419
  if (scrollViewRef.current && data) {
484
420
  // Find the index of the item in the data array
485
- const index = Array.from(data).findIndex(
486
- (dataItem) => dataItem === item
487
- );
421
+ const index = data.findIndex((dataItem) => dataItem === item);
488
422
  if (index >= 0) {
489
423
  handlerMethods.scrollToIndex({
490
424
  index,
@@ -504,7 +438,7 @@ export function useRecyclerViewController<T>(
504
438
  return recyclerViewManager.getWindowSize();
505
439
  },
506
440
  getLayout: (index: number) => {
507
- return recyclerViewManager.getLayout(index);
441
+ return recyclerViewManager.tryGetLayout(index);
508
442
  },
509
443
  getAbsoluteLastScrollOffset: () => {
510
444
  return recyclerViewManager.getAbsoluteLastScrollOffset();
@@ -515,11 +449,11 @@ export function useRecyclerViewController<T>(
515
449
  recordInteraction: () => {
516
450
  recyclerViewManager.recordInteraction();
517
451
  },
518
- getVisibleIndices: () => {
519
- return recyclerViewManager.getVisibleIndices();
452
+ computeVisibleIndices: () => {
453
+ return recyclerViewManager.computeVisibleIndices();
520
454
  },
521
455
  getFirstVisibleIndex: () => {
522
- return recyclerViewManager.getVisibleIndices().startIndex;
456
+ return recyclerViewManager.computeVisibleIndices().startIndex;
523
457
  },
524
458
  recomputeViewableItems: () => {
525
459
  recyclerViewManager.recomputeViewableItems();
@@ -528,10 +462,55 @@ export function useRecyclerViewController<T>(
528
462
  * Disables item recycling in preparation for layout animations.
529
463
  */
530
464
  prepareForLayoutAnimationRender: () => {
531
- recyclerViewManager.disableRecycling = true;
465
+ recyclerViewManager.disableRecycling(true);
532
466
  },
533
467
  };
534
- }, [horizontal, data, recyclerViewManager]);
468
+ }, [
469
+ recyclerViewManager,
470
+ scrollViewRef,
471
+ setTimeout,
472
+ isUnmounted,
473
+ updateScrollOffsetAsync,
474
+ ]);
475
+
476
+ const applyInitialScrollIndex = useCallback(() => {
477
+ const { horizontal, data } = recyclerViewManager.props;
478
+
479
+ const initialScrollIndex =
480
+ recyclerViewManager.getInitialScrollIndex() ?? -1;
481
+ const dataLength = data?.length ?? 0;
482
+ if (
483
+ initialScrollIndex >= 0 &&
484
+ initialScrollIndex < dataLength &&
485
+ !initialScrollCompletedRef.current &&
486
+ recyclerViewManager.getIsFirstLayoutComplete()
487
+ ) {
488
+ // Use setTimeout to ensure that we keep trying to scroll on first few renders
489
+ setTimeout(() => {
490
+ initialScrollCompletedRef.current = true;
491
+ pauseOffsetCorrection.current = false;
492
+ }, 100);
493
+
494
+ pauseOffsetCorrection.current = true;
495
+
496
+ const offset = horizontal
497
+ ? recyclerViewManager.getLayout(initialScrollIndex).x
498
+ : recyclerViewManager.getLayout(initialScrollIndex).y;
499
+ handlerMethods.scrollToOffset({
500
+ offset,
501
+ animated: false,
502
+ skipFirstItemOffset: false,
503
+ });
504
+
505
+ setTimeout(() => {
506
+ handlerMethods.scrollToOffset({
507
+ offset,
508
+ animated: false,
509
+ skipFirstItemOffset: false,
510
+ });
511
+ }, 0);
512
+ }
513
+ }, [handlerMethods, recyclerViewManager, setTimeout]);
535
514
 
536
515
  // Expose imperative methods through the ref
537
516
  useImperativeHandle(
@@ -539,8 +518,8 @@ export function useRecyclerViewController<T>(
539
518
  () => {
540
519
  return { ...scrollViewRef.current, ...handlerMethods };
541
520
  },
542
- [handlerMethods]
521
+ [handlerMethods, scrollViewRef]
543
522
  );
544
523
 
545
- return { applyContentOffset, applyInitialScrollIndex };
524
+ return { applyContentOffset, applyInitialScrollIndex, handlerMethods };
546
525
  }
@@ -14,6 +14,8 @@ export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
14
14
 
15
15
  useMemo(() => {
16
16
  recyclerViewManager.updateProps(props);
17
+ // used to update props so rule can be disabled
18
+ // eslint-disable-next-line react-hooks/exhaustive-deps
17
19
  }, [props]);
18
20
 
19
21
  /**
@@ -21,6 +23,8 @@ export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
21
23
  */
22
24
  useMemo(() => {
23
25
  recyclerViewManager.processDataUpdate();
26
+ // used to process data update so rule can be disabled
27
+ // eslint-disable-next-line react-hooks/exhaustive-deps
24
28
  }, [data]);
25
29
 
26
30
  useEffect(() => {
@@ -28,6 +32,8 @@ export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
28
32
  recyclerViewManager.dispose();
29
33
  velocityTracker.cleanUp();
30
34
  };
35
+ // needs to run only on unmount
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
37
  }, []);
32
38
 
33
39
  return { recyclerViewManager, velocityTracker };
@@ -105,7 +105,7 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
105
105
  const ForwardedScrollComponent = React.forwardRef((_props, ref) =>
106
106
  (renderScrollComponent as any)({ ..._props, ref } as any)
107
107
  );
108
- ForwardedScrollComponent.displayName = "CompatScrollView";
108
+ ForwardedScrollComponent.displayName = "CustomScrollView";
109
109
  scrollComponent = ForwardedScrollComponent as any;
110
110
  } else if (renderScrollComponent) {
111
111
  scrollComponent = renderScrollComponent;
@@ -1,10 +1,10 @@
1
1
  import { useCallback, useEffect, useState } from "react";
2
2
 
3
3
  /**
4
- * Hook that provides callbacks which are aware of component unmount state.
5
- * Any timeouts created with these callbacks will be automatically cleared when the component unmounts.
4
+ * Hook that provides a setTimeout which is aware of component unmount state.
5
+ * Any timeouts created with this hook will be automatically cleared when the component unmounts.
6
6
  */
7
- export function useUnmountAwareCallbacks() {
7
+ export function useUnmountAwareTimeout() {
8
8
  // Store active timeout IDs in a Set for more efficient add/remove operations
9
9
  const [timeoutIds] = useState<Set<NodeJS.Timeout>>(() => new Set());
10
10
 
@@ -35,3 +35,39 @@ export function useUnmountAwareCallbacks() {
35
35
  setTimeout,
36
36
  };
37
37
  }
38
+
39
+ /**
40
+ * Hook that provides a requestAnimationFrame which is aware of component unmount state.
41
+ * Any animation frames requested with this hook will be automatically canceled when the component unmounts.
42
+ */
43
+ export function useUnmountAwareAnimationFrame() {
44
+ // Store active animation frame request IDs in a Set for more efficient add/remove operations
45
+ const [requestIds] = useState<Set<number>>(() => new Set());
46
+
47
+ // Cancel all animation frame requests on unmount
48
+ useEffect(() => {
49
+ return () => {
50
+ requestIds.forEach((id) => cancelAnimationFrame(id));
51
+ requestIds.clear();
52
+ };
53
+ }, [requestIds]);
54
+
55
+ // Create a safe requestAnimationFrame that will be canceled on unmount
56
+ const requestAnimationFrame = useCallback(
57
+ (callback: FrameRequestCallback): void => {
58
+ const id = global.requestAnimationFrame((timestamp) => {
59
+ // Remove this request ID from the tracking set
60
+ requestIds.delete(id);
61
+ callback(timestamp);
62
+ });
63
+
64
+ // Track this request ID
65
+ requestIds.add(id);
66
+ },
67
+ [requestIds]
68
+ );
69
+
70
+ return {
71
+ requestAnimationFrame,
72
+ };
73
+ }
@@ -78,12 +78,10 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
78
78
  */
79
79
  getLayoutSize(): RVDimension {
80
80
  if (this.layouts.length === 0) return { width: 0, height: 0 };
81
- const lastRowTallestItem = this.processAndReturnTallestItemInRow(
82
- this.layouts.length - 1
83
- );
81
+ const totalHeight = this.computeTotalHeight(this.layouts.length - 1);
84
82
  return {
85
83
  width: this.boundedSize,
86
- height: lastRowTallestItem.y + lastRowTallestItem.height,
84
+ height: totalHeight,
87
85
  };
88
86
  }
89
87
 
@@ -93,7 +91,9 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
93
91
  * @param endIndex Ending index of items to recompute
94
92
  */
95
93
  recomputeLayouts(startIndex: number, endIndex: number): void {
96
- const newStartIndex = this.locateFirstNeighbourIndex(startIndex);
94
+ const newStartIndex = this.locateFirstNeighbourIndex(
95
+ Math.max(0, startIndex - 1)
96
+ );
97
97
  const startVal = this.getLayout(newStartIndex);
98
98
 
99
99
  let startX = startVal.x;
@@ -102,7 +102,7 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
102
102
  for (let i = newStartIndex; i <= endIndex; i++) {
103
103
  const layout = this.getLayout(i);
104
104
  if (!this.checkBounds(startX, layout.width)) {
105
- const tallestItem = this.processAndReturnTallestItemInRow(i);
105
+ const tallestItem = this.processAndReturnTallestItemInRow(i - 1);
106
106
  startY = tallestItem.y + tallestItem.height;
107
107
  startX = 0;
108
108
  }
@@ -111,6 +111,9 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
111
111
  layout.y = startY;
112
112
  startX += layout.width;
113
113
  }
114
+ if (endIndex === this.layouts.length - 1) {
115
+ this.processAndReturnTallestItemInRow(endIndex);
116
+ }
114
117
  }
115
118
 
116
119
  /**
@@ -182,6 +185,26 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
182
185
  return tallestItem;
183
186
  }
184
187
 
188
+ /**
189
+ * Computes the total height of the layout.
190
+ * @param index Index of the last item in the layout
191
+ * @returns Total height of the layout
192
+ */
193
+ private computeTotalHeight(index: number): number {
194
+ const startIndex = this.locateFirstNeighbourIndex(index);
195
+ const y = this.layouts[startIndex].y;
196
+ let maxHeight = 0;
197
+ let i = startIndex;
198
+ while (Math.ceil(this.layouts[i].y) === Math.ceil(y)) {
199
+ maxHeight = Math.max(maxHeight, this.layouts[i].height);
200
+ i++;
201
+ if (i >= this.layouts.length) {
202
+ break;
203
+ }
204
+ }
205
+ return y + maxHeight;
206
+ }
207
+
185
208
  /**
186
209
  * Checks if an item can fit within the bounded width.
187
210
  * @param itemX Starting X position of the item
@@ -201,7 +224,7 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
201
224
  if (startIndex === 0) {
202
225
  return 0;
203
226
  }
204
- let i = startIndex - 1;
227
+ let i = startIndex;
205
228
  for (; i >= 0; i--) {
206
229
  if (this.layouts[i].x === 0) {
207
230
  break;
@@ -114,8 +114,8 @@ export abstract class RVLayoutManager {
114
114
  /**
115
115
  * Gets indices of items currently visible in the viewport.
116
116
  * Uses binary search for efficient lookup.
117
- * @param unboundDimensionStart Start position of viewport
118
- * @param unboundDimensionEnd End position of viewport
117
+ * @param unboundDimensionStart Start position of viewport (start X or start Y)
118
+ * @param unboundDimensionEnd End position of viewport (end X or end Y)
119
119
  * @returns ConsecutiveNumbers containing visible indices
120
120
  */
121
121
  getVisibleLayouts(
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=RecycleKeyManager.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"RecycleKeyManager.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/RecycleKeyManager.test.ts"],"names":[],"mappings":""}