@shopify/flash-list 2.0.0-alpha.9 → 2.0.0-rc.1

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 (180) hide show
  1. package/README.md +37 -97
  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 +15 -8
  10. package/dist/FlashListProps.d.ts.map +1 -1
  11. package/dist/FlashListProps.js.map +1 -1
  12. package/dist/FlashListRef.d.ts +305 -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/MasonryFlashList.js.map +1 -1
  17. package/dist/__tests__/RecyclerView.test.js +62 -27
  18. package/dist/__tests__/RecyclerView.test.js.map +1 -1
  19. package/dist/__tests__/RenderStackManager.test.d.ts +2 -0
  20. package/dist/__tests__/RenderStackManager.test.d.ts.map +1 -0
  21. package/dist/__tests__/RenderStackManager.test.js +486 -0
  22. package/dist/__tests__/RenderStackManager.test.js.map +1 -0
  23. package/dist/__tests__/helpers/createLayoutManager.d.ts.map +1 -1
  24. package/dist/__tests__/helpers/createLayoutManager.js +3 -4
  25. package/dist/__tests__/helpers/createLayoutManager.js.map +1 -1
  26. package/dist/__tests__/useUnmountAwareCallbacks.test.js +1 -1
  27. package/dist/__tests__/useUnmountAwareCallbacks.test.js.map +1 -1
  28. package/dist/benchmark/useFlatListBenchmark.js +8 -7
  29. package/dist/benchmark/useFlatListBenchmark.js.map +1 -1
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/native/config/PlatformHelper.android.d.ts +1 -0
  34. package/dist/native/config/PlatformHelper.android.d.ts.map +1 -1
  35. package/dist/native/config/PlatformHelper.android.js +1 -0
  36. package/dist/native/config/PlatformHelper.android.js.map +1 -1
  37. package/dist/native/config/PlatformHelper.d.ts +1 -0
  38. package/dist/native/config/PlatformHelper.d.ts.map +1 -1
  39. package/dist/native/config/PlatformHelper.ios.d.ts +1 -0
  40. package/dist/native/config/PlatformHelper.ios.d.ts.map +1 -1
  41. package/dist/native/config/PlatformHelper.ios.js +1 -0
  42. package/dist/native/config/PlatformHelper.ios.js.map +1 -1
  43. package/dist/native/config/PlatformHelper.js +1 -0
  44. package/dist/native/config/PlatformHelper.js.map +1 -1
  45. package/dist/native/config/PlatformHelper.web.d.ts +1 -0
  46. package/dist/native/config/PlatformHelper.web.d.ts.map +1 -1
  47. package/dist/native/config/PlatformHelper.web.js +1 -0
  48. package/dist/native/config/PlatformHelper.web.js.map +1 -1
  49. package/dist/recyclerview/RecyclerView.d.ts +2 -1
  50. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  51. package/dist/recyclerview/RecyclerView.js +63 -45
  52. package/dist/recyclerview/RecyclerView.js.map +1 -1
  53. package/dist/recyclerview/RecyclerViewContextProvider.d.ts +6 -5
  54. package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -1
  55. package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -1
  56. package/dist/recyclerview/RecyclerViewManager.d.ts +21 -7
  57. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
  58. package/dist/recyclerview/RecyclerViewManager.js +105 -113
  59. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  60. package/dist/recyclerview/RenderStackManager.d.ts +85 -0
  61. package/dist/recyclerview/RenderStackManager.d.ts.map +1 -0
  62. package/dist/recyclerview/RenderStackManager.js +324 -0
  63. package/dist/recyclerview/RenderStackManager.js.map +1 -0
  64. package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
  65. package/dist/recyclerview/ViewHolder.js +5 -3
  66. package/dist/recyclerview/ViewHolder.js.map +1 -1
  67. package/dist/recyclerview/ViewHolderCollection.d.ts +3 -1
  68. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
  69. package/dist/recyclerview/ViewHolderCollection.js +23 -8
  70. package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
  71. package/dist/recyclerview/components/ScrollAnchor.d.ts +2 -2
  72. package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -1
  73. package/dist/recyclerview/components/ScrollAnchor.js +9 -5
  74. package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
  75. package/dist/recyclerview/components/StickyHeaders.d.ts +1 -1
  76. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
  77. package/dist/recyclerview/components/StickyHeaders.js +39 -32
  78. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  79. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts +45 -1
  80. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts.map +1 -1
  81. package/dist/recyclerview/helpers/EngagedIndicesTracker.js +77 -20
  82. package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -1
  83. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts +10 -0
  84. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts.map +1 -0
  85. package/dist/recyclerview/helpers/RenderTimeTracker.js +39 -0
  86. package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -0
  87. package/dist/recyclerview/helpers/VelocityTracker.d.ts +29 -0
  88. package/dist/recyclerview/helpers/VelocityTracker.d.ts.map +1 -0
  89. package/dist/recyclerview/helpers/VelocityTracker.js +70 -0
  90. package/dist/recyclerview/helpers/VelocityTracker.js.map +1 -0
  91. package/dist/recyclerview/hooks/useBoundDetection.d.ts +1 -2
  92. package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -1
  93. package/dist/recyclerview/hooks/useBoundDetection.js +19 -16
  94. package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
  95. package/dist/recyclerview/hooks/useMappingHelper.d.ts +1 -1
  96. package/dist/recyclerview/hooks/useMappingHelper.d.ts.map +1 -1
  97. package/dist/recyclerview/hooks/useMappingHelper.js +1 -1
  98. package/dist/recyclerview/hooks/useMappingHelper.js.map +1 -1
  99. package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -1
  100. package/dist/recyclerview/hooks/useOnLoad.js +4 -6
  101. package/dist/recyclerview/hooks/useOnLoad.js.map +1 -1
  102. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +3 -48
  103. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  104. package/dist/recyclerview/hooks/useRecyclerViewController.js +174 -123
  105. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  106. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts +2 -0
  107. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
  108. package/dist/recyclerview/hooks/useRecyclerViewManager.js +10 -1
  109. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
  110. package/dist/recyclerview/hooks/useSecondaryProps.js +1 -1
  111. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts +10 -3
  112. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts.map +1 -1
  113. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js +33 -4
  114. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js.map +1 -1
  115. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +6 -0
  116. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  117. package/dist/recyclerview/layout-managers/GridLayoutManager.js +27 -5
  118. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  119. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +10 -16
  120. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
  121. package/dist/recyclerview/layout-managers/LayoutManager.js +4 -14
  122. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
  123. package/dist/tsconfig.tsbuildinfo +1 -1
  124. package/dist/viewability/ViewToken.d.ts +2 -2
  125. package/dist/viewability/ViewToken.d.ts.map +1 -1
  126. package/dist/viewability/ViewabilityHelper.js +1 -1
  127. package/dist/viewability/ViewabilityHelper.js.map +1 -1
  128. package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
  129. package/dist/viewability/ViewabilityManager.js +1 -2
  130. package/dist/viewability/ViewabilityManager.js.map +1 -1
  131. package/jestSetup.js +30 -11
  132. package/package.json +2 -1
  133. package/src/AnimatedFlashList.ts +3 -2
  134. package/src/FlashList.tsx +24 -0
  135. package/src/FlashListProps.ts +20 -8
  136. package/src/FlashListRef.ts +320 -0
  137. package/src/MasonryFlashList.tsx +2 -2
  138. package/src/__tests__/RecyclerView.test.tsx +83 -29
  139. package/src/__tests__/RenderStackManager.test.ts +575 -0
  140. package/src/__tests__/helpers/createLayoutManager.ts +2 -3
  141. package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
  142. package/src/benchmark/useFlatListBenchmark.ts +2 -2
  143. package/src/index.ts +1 -0
  144. package/src/native/config/PlatformHelper.android.ts +1 -0
  145. package/src/native/config/PlatformHelper.ios.ts +1 -0
  146. package/src/native/config/PlatformHelper.ts +1 -0
  147. package/src/native/config/PlatformHelper.web.ts +1 -0
  148. package/src/recyclerview/RecyclerView.tsx +82 -52
  149. package/src/recyclerview/RecyclerViewContextProvider.ts +12 -6
  150. package/src/recyclerview/RecyclerViewManager.ts +123 -98
  151. package/src/recyclerview/RenderStackManager.ts +291 -0
  152. package/src/recyclerview/ViewHolder.tsx +5 -3
  153. package/src/recyclerview/ViewHolderCollection.tsx +33 -12
  154. package/src/recyclerview/components/ScrollAnchor.tsx +21 -9
  155. package/src/recyclerview/components/StickyHeaders.tsx +62 -44
  156. package/src/recyclerview/helpers/EngagedIndicesTracker.ts +118 -23
  157. package/src/recyclerview/helpers/RenderTimeTracker.ts +38 -0
  158. package/src/recyclerview/helpers/VelocityTracker.ts +77 -0
  159. package/src/recyclerview/hooks/useBoundDetection.ts +25 -18
  160. package/src/recyclerview/hooks/useMappingHelper.ts +1 -1
  161. package/src/recyclerview/hooks/useOnLoad.ts +4 -6
  162. package/src/recyclerview/hooks/useRecyclerViewController.tsx +199 -176
  163. package/src/recyclerview/hooks/useRecyclerViewManager.ts +11 -1
  164. package/src/recyclerview/hooks/useSecondaryProps.tsx +1 -1
  165. package/src/recyclerview/hooks/useUnmountAwareCallbacks.ts +39 -3
  166. package/src/recyclerview/layout-managers/GridLayoutManager.ts +30 -7
  167. package/src/recyclerview/layout-managers/LayoutManager.ts +12 -21
  168. package/src/viewability/ViewToken.ts +2 -2
  169. package/src/viewability/ViewabilityHelper.ts +1 -1
  170. package/src/viewability/ViewabilityManager.ts +6 -3
  171. package/dist/__tests__/RecycleKeyManager.test.d.ts +0 -2
  172. package/dist/__tests__/RecycleKeyManager.test.d.ts.map +0 -1
  173. package/dist/__tests__/RecycleKeyManager.test.js +0 -210
  174. package/dist/__tests__/RecycleKeyManager.test.js.map +0 -1
  175. package/dist/recyclerview/RecycleKeyManager.d.ts +0 -82
  176. package/dist/recyclerview/RecycleKeyManager.d.ts.map +0 -1
  177. package/dist/recyclerview/RecycleKeyManager.js +0 -135
  178. package/dist/recyclerview/RecycleKeyManager.js.map +0 -1
  179. package/src/__tests__/RecycleKeyManager.test.ts +0 -254
  180. 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
- const pauseAdjustRef = useRef(false);
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
- pauseAdjustRef.current = false;
123
- }, 100);
124
-
125
- pauseAdjustRef.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,19 @@ 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 } = recyclerViewManager.props;
176
93
  // Resolve all pending scroll updates from previous calls
177
94
  const resolves = pendingScrollResolves.current;
178
95
  pendingScrollResolves.current = [];
179
96
  resolves.forEach((resolve) => resolve());
180
97
 
181
- const currentDataLength = props.data?.length ?? 0;
98
+ const currentDataLength = data?.length ?? 0;
182
99
 
183
100
  if (
184
- !props.horizontal &&
185
101
  recyclerViewManager.getIsFirstLayoutComplete() &&
186
- props.keyExtractor &&
102
+ keyExtractor &&
187
103
  currentDataLength > 0 &&
188
- props.maintainVisibleContentPosition?.disabled !== true
104
+ recyclerViewManager.shouldMaintainVisibleContentPosition()
189
105
  ) {
190
106
  const hasDataChanged = currentDataLength !== lastDataLengthRef.current;
191
107
  // If we have a tracked first visible item, maintain its position
@@ -195,34 +111,45 @@ export function useRecyclerViewController<T>(
195
111
  .getEngagedIndices()
196
112
  .findValue(
197
113
  (index) =>
198
- props.keyExtractor?.(props.data![index], index) ===
114
+ keyExtractor?.(data![index], index) ===
199
115
  firstVisibleItemKey.current
200
116
  ) ??
201
117
  (hasDataChanged
202
- ? props.data?.findIndex(
118
+ ? data?.findIndex(
203
119
  (item, index) =>
204
- props.keyExtractor?.(item, index) ===
205
- firstVisibleItemKey.current
120
+ keyExtractor?.(item, index) === firstVisibleItemKey.current
206
121
  )
207
122
  : undefined);
208
123
 
209
- if (currentIndexOfFirstVisibleItem !== undefined) {
124
+ if (
125
+ currentIndexOfFirstVisibleItem !== undefined &&
126
+ currentIndexOfFirstVisibleItem >= 0
127
+ ) {
210
128
  // Calculate the difference in position and apply the offset
211
- const diff =
212
- recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem).y -
213
- firstVisibleItemLayout.current!.y;
129
+ const diff = horizontal
130
+ ? recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem).x -
131
+ firstVisibleItemLayout.current!.x
132
+ : recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem).y -
133
+ firstVisibleItemLayout.current!.y;
214
134
  firstVisibleItemLayout.current = {
215
135
  ...recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem),
216
136
  };
217
- if (diff !== 0 && !pauseAdjustRef.current) {
137
+ if (diff !== 0 && !pauseOffsetCorrection.current) {
218
138
  // console.log("diff", diff, firstVisibleItemKey.current);
219
139
  if (PlatformConfig.supportsOffsetCorrection) {
140
+ // console.log("scrollBy", diff);
220
141
  scrollAnchorRef.current?.scrollBy(diff);
221
142
  } else {
222
- scrollViewRef.current?.scrollTo({
223
- y: recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
224
- animated: false,
225
- });
143
+ const scrollToParams = horizontal
144
+ ? {
145
+ x: recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
146
+ animated: false,
147
+ }
148
+ : {
149
+ y: recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
150
+ animated: false,
151
+ };
152
+ scrollViewRef.current?.scrollTo(scrollToParams);
226
153
  }
227
154
  if (hasDataChanged) {
228
155
  updateScrollOffsetAsync(
@@ -240,11 +167,11 @@ export function useRecyclerViewController<T>(
240
167
  // Update the tracked first visible item
241
168
  const firstVisibleIndex = Math.max(
242
169
  0,
243
- recyclerViewManager.getVisibleIndices().startIndex
170
+ recyclerViewManager.computeVisibleIndices().startIndex
244
171
  );
245
172
  if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
246
- firstVisibleItemKey.current = props.keyExtractor(
247
- props.data![firstVisibleIndex],
173
+ firstVisibleItemKey.current = keyExtractor(
174
+ data![firstVisibleIndex],
248
175
  firstVisibleIndex
249
176
  );
250
177
  firstVisibleItemLayout.current = {
@@ -252,12 +179,20 @@ export function useRecyclerViewController<T>(
252
179
  };
253
180
  }
254
181
  }
255
- lastDataLengthRef.current = props.data?.length ?? 0;
256
- }, [props.data, props.keyExtractor, recyclerViewManager, setTimeout]);
257
-
258
- const handlerMethods = useMemo(() => {
182
+ lastDataLengthRef.current = data?.length ?? 0;
183
+ }, [
184
+ recyclerViewManager,
185
+ scrollAnchorRef,
186
+ scrollViewRef,
187
+ setTimeout,
188
+ updateScrollOffsetAsync,
189
+ ]);
190
+
191
+ const handlerMethods: FlashListRef<T> = useMemo(() => {
259
192
  return {
260
- props,
193
+ get props() {
194
+ return recyclerViewManager.props;
195
+ },
261
196
  /**
262
197
  * Scrolls the list to a specific offset position.
263
198
  * Handles RTL layouts and first item offset adjustments.
@@ -267,6 +202,7 @@ export function useRecyclerViewController<T>(
267
202
  animated,
268
203
  skipFirstItemOffset = true,
269
204
  }: ScrollToOffsetParams) => {
205
+ const { horizontal } = recyclerViewManager.props;
270
206
  if (scrollViewRef.current) {
271
207
  // Adjust offset for RTL layouts in horizontal mode
272
208
  if (I18nManager.isRTL && horizontal) {
@@ -295,6 +231,9 @@ export function useRecyclerViewController<T>(
295
231
  });
296
232
  }
297
233
  },
234
+ clearLayoutCacheOnUpdate: () => {
235
+ recyclerViewManager.markLayoutManagerDirty();
236
+ },
298
237
 
299
238
  // Expose native scroll view methods
300
239
  flashScrollIndicators: () => {
@@ -314,13 +253,16 @@ export function useRecyclerViewController<T>(
314
253
  * Scrolls to the end of the list.
315
254
  */
316
255
  scrollToEnd: async ({ animated }: ScrollToEdgeParams = {}) => {
256
+ const { data } = recyclerViewManager.props;
317
257
  if (data && data.length > 0) {
318
258
  await handlerMethods.scrollToIndex({
319
259
  index: data.length - 1,
320
260
  animated,
321
261
  });
322
262
  }
323
- scrollViewRef.current!.scrollToEnd({ animated });
263
+ setTimeout(() => {
264
+ scrollViewRef.current!.scrollToEnd({ animated });
265
+ }, 0);
324
266
  },
325
267
 
326
268
  /**
@@ -343,8 +285,15 @@ export function useRecyclerViewController<T>(
343
285
  viewPosition,
344
286
  viewOffset,
345
287
  }: ScrollToIndexParams) => {
346
- if (scrollViewRef.current && data && data.length > index) {
347
- pauseAdjustRef.current = true;
288
+ const { horizontal } = recyclerViewManager.props;
289
+ if (
290
+ scrollViewRef.current &&
291
+ index >= 0 &&
292
+ index < recyclerViewManager.getDataLength()
293
+ ) {
294
+ // Pause the scroll offset adjustments
295
+ pauseOffsetCorrection.current = true;
296
+ recyclerViewManager.setOffsetProjectionEnabled(false);
348
297
 
349
298
  const getFinalOffset = () => {
350
299
  const layout = recyclerViewManager.getLayout(index);
@@ -368,74 +317,104 @@ export function useRecyclerViewController<T>(
368
317
  finalOffset += viewOffset;
369
318
  }
370
319
  }
371
- return finalOffset;
320
+ return finalOffset + recyclerViewManager.firstItemOffset;
372
321
  };
373
- let lastScrollOffset = recyclerViewManager.getLastScrollOffset();
374
- let finalOffset = getFinalOffset();
322
+ const lastAbsoluteScrollOffset =
323
+ recyclerViewManager.getAbsoluteLastScrollOffset();
375
324
  const bufferForScroll = horizontal
376
325
  ? recyclerViewManager.getWindowSize().width
377
326
  : recyclerViewManager.getWindowSize().height;
378
327
 
379
328
  const bufferForCompute = bufferForScroll * 2;
380
329
 
381
- if (finalOffset > lastScrollOffset) {
382
- lastScrollOffset = Math.max(
383
- finalOffset - bufferForCompute,
384
- lastScrollOffset
385
- );
386
- recyclerViewManager.setScrollDirection("forward");
387
- } else {
388
- lastScrollOffset = Math.min(
389
- finalOffset + bufferForCompute,
390
- lastScrollOffset
391
- );
392
- recyclerViewManager.setScrollDirection("backward");
393
- }
330
+ const getStartScrollOffset = () => {
331
+ let lastScrollOffset = lastAbsoluteScrollOffset;
332
+ const finalOffset = getFinalOffset();
394
333
 
395
- if (animated) {
396
- // go from finalOffset to lastScrollOffset in 5 steps
397
- for (let i = 0; i < 5; i++) {
398
- if (isUnmounted.current) {
399
- return;
400
- }
401
- await updateScrollOffsetAsync(
402
- finalOffset + (lastScrollOffset - finalOffset) * (i / 4)
334
+ if (finalOffset > lastScrollOffset) {
335
+ lastScrollOffset = Math.max(
336
+ finalOffset - bufferForCompute,
337
+ lastScrollOffset
403
338
  );
404
- }
405
- } else {
406
- // go from lastScrollOffset to finalOffset in 5 steps
407
- for (let i = 0; i < 5; i++) {
408
- if (isUnmounted.current) {
409
- return;
410
- }
411
- await updateScrollOffsetAsync(
412
- lastScrollOffset + (finalOffset - lastScrollOffset) * (i / 4)
339
+ recyclerViewManager.setScrollDirection("forward");
340
+ } else {
341
+ lastScrollOffset = Math.min(
342
+ finalOffset + bufferForCompute,
343
+ lastScrollOffset
413
344
  );
345
+ recyclerViewManager.setScrollDirection("backward");
346
+ }
347
+ return lastScrollOffset;
348
+ };
349
+ let initialTargetOffset = getFinalOffset();
350
+ let initialStartScrollOffset = getStartScrollOffset();
351
+ let finalOffset = initialTargetOffset;
352
+ let startScrollOffset = initialStartScrollOffset;
353
+
354
+ const steps = 5;
355
+ // go from finalOffset to startScrollOffset in 5 steps for animated
356
+ // otherwise go from startScrollOffset to finalOffset in 5 steps
357
+ for (let i = 0; i < steps; i++) {
358
+ if (isUnmounted.current) {
359
+ return;
360
+ }
361
+ const nextOffset = animated
362
+ ? finalOffset +
363
+ (startScrollOffset - finalOffset) * (i / (steps - 1))
364
+ : startScrollOffset +
365
+ (finalOffset - startScrollOffset) * (i / (steps - 1));
366
+ await updateScrollOffsetAsync(nextOffset);
367
+
368
+ // In case some change happens in the middle of this operation
369
+ // and the index is out of bounds, scroll to the end
370
+ if (index >= recyclerViewManager.getDataLength()) {
371
+ handlerMethods.scrollToEnd({ animated });
372
+ return;
373
+ }
374
+
375
+ const newFinalOffset = getFinalOffset();
376
+ if (
377
+ (newFinalOffset < initialTargetOffset &&
378
+ newFinalOffset < initialStartScrollOffset) ||
379
+ (newFinalOffset > initialTargetOffset &&
380
+ newFinalOffset > initialStartScrollOffset)
381
+ ) {
382
+ finalOffset = newFinalOffset;
383
+ startScrollOffset = getStartScrollOffset();
384
+ initialTargetOffset = newFinalOffset;
385
+ initialStartScrollOffset = startScrollOffset;
386
+ i = -1; // Restart compute loop
414
387
  }
415
388
  }
389
+
416
390
  finalOffset = getFinalOffset();
417
391
  const maxOffset = recyclerViewManager.getMaxScrollOffset();
418
392
 
419
393
  if (finalOffset > maxOffset) {
420
394
  finalOffset = maxOffset;
421
395
  }
396
+
422
397
  if (animated) {
423
- // We don't need to add firstItemOffset here as it will be added in scrollToOffset
398
+ // We don't need to add firstItemOffset here as it's already added
424
399
  handlerMethods.scrollToOffset({
425
- offset: lastScrollOffset,
400
+ offset: startScrollOffset,
426
401
  animated: false,
427
- skipFirstItemOffset: false,
402
+ skipFirstItemOffset: true,
428
403
  });
429
404
  }
430
405
  handlerMethods.scrollToOffset({
431
406
  offset: finalOffset,
432
407
  animated,
433
- skipFirstItemOffset: false,
408
+ skipFirstItemOffset: true,
434
409
  });
435
410
 
436
- setTimeout(() => {
437
- pauseAdjustRef.current = false;
438
- }, 200);
411
+ setTimeout(
412
+ () => {
413
+ pauseOffsetCorrection.current = false;
414
+ recyclerViewManager.setOffsetProjectionEnabled(true);
415
+ },
416
+ animated ? 300 : 200
417
+ );
439
418
  }
440
419
  },
441
420
 
@@ -449,11 +428,10 @@ export function useRecyclerViewController<T>(
449
428
  viewPosition,
450
429
  viewOffset,
451
430
  }: ScrollToItemParams<T>) => {
431
+ const { data } = recyclerViewManager.props;
452
432
  if (scrollViewRef.current && data) {
453
433
  // Find the index of the item in the data array
454
- const index = Array.from(data).findIndex(
455
- (dataItem) => dataItem === item
456
- );
434
+ const index = data.findIndex((dataItem) => dataItem === item);
457
435
  if (index >= 0) {
458
436
  handlerMethods.scrollToIndex({
459
437
  index,
@@ -473,7 +451,7 @@ export function useRecyclerViewController<T>(
473
451
  return recyclerViewManager.getWindowSize();
474
452
  },
475
453
  getLayout: (index: number) => {
476
- return recyclerViewManager.getLayout(index);
454
+ return recyclerViewManager.tryGetLayout(index);
477
455
  },
478
456
  getAbsoluteLastScrollOffset: () => {
479
457
  return recyclerViewManager.getAbsoluteLastScrollOffset();
@@ -484,11 +462,11 @@ export function useRecyclerViewController<T>(
484
462
  recordInteraction: () => {
485
463
  recyclerViewManager.recordInteraction();
486
464
  },
487
- getVisibleIndices: () => {
488
- return recyclerViewManager.getVisibleIndices();
465
+ computeVisibleIndices: () => {
466
+ return recyclerViewManager.computeVisibleIndices();
489
467
  },
490
468
  getFirstVisibleIndex: () => {
491
- return recyclerViewManager.getVisibleIndices().startIndex;
469
+ return recyclerViewManager.computeVisibleIndices().startIndex;
492
470
  },
493
471
  recomputeViewableItems: () => {
494
472
  recyclerViewManager.recomputeViewableItems();
@@ -497,10 +475,55 @@ export function useRecyclerViewController<T>(
497
475
  * Disables item recycling in preparation for layout animations.
498
476
  */
499
477
  prepareForLayoutAnimationRender: () => {
500
- recyclerViewManager.disableRecycling = true;
478
+ recyclerViewManager.disableRecycling(true);
501
479
  },
502
480
  };
503
- }, [horizontal, data, recyclerViewManager]);
481
+ }, [
482
+ recyclerViewManager,
483
+ scrollViewRef,
484
+ setTimeout,
485
+ isUnmounted,
486
+ updateScrollOffsetAsync,
487
+ ]);
488
+
489
+ const applyInitialScrollIndex = useCallback(() => {
490
+ const { horizontal, data } = recyclerViewManager.props;
491
+
492
+ const initialScrollIndex =
493
+ recyclerViewManager.getInitialScrollIndex() ?? -1;
494
+ const dataLength = data?.length ?? 0;
495
+ if (
496
+ initialScrollIndex >= 0 &&
497
+ initialScrollIndex < dataLength &&
498
+ !initialScrollCompletedRef.current &&
499
+ recyclerViewManager.getIsFirstLayoutComplete()
500
+ ) {
501
+ // Use setTimeout to ensure that we keep trying to scroll on first few renders
502
+ setTimeout(() => {
503
+ initialScrollCompletedRef.current = true;
504
+ pauseOffsetCorrection.current = false;
505
+ }, 100);
506
+
507
+ pauseOffsetCorrection.current = true;
508
+
509
+ const offset = horizontal
510
+ ? recyclerViewManager.getLayout(initialScrollIndex).x
511
+ : recyclerViewManager.getLayout(initialScrollIndex).y;
512
+ handlerMethods.scrollToOffset({
513
+ offset,
514
+ animated: false,
515
+ skipFirstItemOffset: false,
516
+ });
517
+
518
+ setTimeout(() => {
519
+ handlerMethods.scrollToOffset({
520
+ offset,
521
+ animated: false,
522
+ skipFirstItemOffset: false,
523
+ });
524
+ }, 0);
525
+ }
526
+ }, [handlerMethods, recyclerViewManager, setTimeout]);
504
527
 
505
528
  // Expose imperative methods through the ref
506
529
  useImperativeHandle(
@@ -508,8 +531,8 @@ export function useRecyclerViewController<T>(
508
531
  () => {
509
532
  return { ...scrollViewRef.current, ...handlerMethods };
510
533
  },
511
- [handlerMethods]
534
+ [handlerMethods, scrollViewRef]
512
535
  );
513
536
 
514
- return { applyContentOffset, applyInitialScrollIndex };
537
+ return { applyContentOffset, applyInitialScrollIndex, handlerMethods };
515
538
  }
@@ -2,15 +2,20 @@ import { useEffect, useMemo, useState } from "react";
2
2
 
3
3
  import { RecyclerViewProps } from "../RecyclerViewProps";
4
4
  import { RecyclerViewManager } from "../RecyclerViewManager";
5
+ import { VelocityTracker } from "../helpers/VelocityTracker";
5
6
 
6
7
  export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
7
8
  const [recyclerViewManager] = useState<RecyclerViewManager<T>>(
8
9
  () => new RecyclerViewManager(props)
9
10
  );
11
+ const [velocityTracker] = useState(() => new VelocityTracker());
12
+
10
13
  const { data } = props;
11
14
 
12
15
  useMemo(() => {
13
16
  recyclerViewManager.updateProps(props);
17
+ // used to update props so rule can be disabled
18
+ // eslint-disable-next-line react-hooks/exhaustive-deps
14
19
  }, [props]);
15
20
 
16
21
  /**
@@ -18,13 +23,18 @@ export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
18
23
  */
19
24
  useMemo(() => {
20
25
  recyclerViewManager.processDataUpdate();
26
+ // used to process data update so rule can be disabled
27
+ // eslint-disable-next-line react-hooks/exhaustive-deps
21
28
  }, [data]);
22
29
 
23
30
  useEffect(() => {
24
31
  return () => {
25
32
  recyclerViewManager.dispose();
33
+ velocityTracker.cleanUp();
26
34
  };
35
+ // needs to run only on unmount
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
27
37
  }, []);
28
38
 
29
- return { recyclerViewManager };
39
+ return { recyclerViewManager, velocityTracker };
30
40
  };
@@ -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
+ }