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

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 (209) hide show
  1. package/README.md +37 -97
  2. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/BlankAreaEvent.kt +2 -2
  3. package/dist/AnimatedFlashList.d.ts.map +1 -1
  4. package/dist/AnimatedFlashList.js +3 -3
  5. package/dist/AnimatedFlashList.js.map +1 -1
  6. package/dist/FlashList.d.ts +9 -0
  7. package/dist/FlashList.d.ts.map +1 -1
  8. package/dist/FlashList.js +20 -0
  9. package/dist/FlashList.js.map +1 -1
  10. package/dist/FlashListProps.d.ts +30 -10
  11. package/dist/FlashListProps.d.ts.map +1 -1
  12. package/dist/FlashListProps.js.map +1 -1
  13. package/dist/FlashListRef.d.ts +305 -0
  14. package/dist/FlashListRef.d.ts.map +1 -0
  15. package/dist/FlashListRef.js +3 -0
  16. package/dist/FlashListRef.js.map +1 -0
  17. package/dist/MasonryFlashList.js.map +1 -1
  18. package/dist/__tests__/RecyclerView.test.js +72 -28
  19. package/dist/__tests__/RecyclerView.test.js.map +1 -1
  20. package/dist/__tests__/RenderStackManager.test.d.ts +2 -0
  21. package/dist/__tests__/RenderStackManager.test.d.ts.map +1 -0
  22. package/dist/__tests__/RenderStackManager.test.js +485 -0
  23. package/dist/__tests__/RenderStackManager.test.js.map +1 -0
  24. package/dist/__tests__/helpers/createLayoutManager.d.ts.map +1 -1
  25. package/dist/__tests__/helpers/createLayoutManager.js +3 -4
  26. package/dist/__tests__/helpers/createLayoutManager.js.map +1 -1
  27. package/dist/__tests__/useUnmountAwareCallbacks.test.js +1 -1
  28. package/dist/__tests__/useUnmountAwareCallbacks.test.js.map +1 -1
  29. package/dist/benchmark/useBenchmark.js +0 -25
  30. package/dist/benchmark/useBenchmark.js.map +1 -1
  31. package/dist/benchmark/useFlatListBenchmark.js +8 -7
  32. package/dist/benchmark/useFlatListBenchmark.js.map +1 -1
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/native/config/PlatformHelper.android.d.ts +1 -0
  38. package/dist/native/config/PlatformHelper.android.d.ts.map +1 -1
  39. package/dist/native/config/PlatformHelper.android.js +1 -0
  40. package/dist/native/config/PlatformHelper.android.js.map +1 -1
  41. package/dist/native/config/PlatformHelper.d.ts +1 -0
  42. package/dist/native/config/PlatformHelper.d.ts.map +1 -1
  43. package/dist/native/config/PlatformHelper.ios.d.ts +1 -0
  44. package/dist/native/config/PlatformHelper.ios.d.ts.map +1 -1
  45. package/dist/native/config/PlatformHelper.ios.js +1 -0
  46. package/dist/native/config/PlatformHelper.ios.js.map +1 -1
  47. package/dist/native/config/PlatformHelper.js +1 -0
  48. package/dist/native/config/PlatformHelper.js.map +1 -1
  49. package/dist/native/config/PlatformHelper.web.d.ts +1 -0
  50. package/dist/native/config/PlatformHelper.web.d.ts.map +1 -1
  51. package/dist/native/config/PlatformHelper.web.js +1 -0
  52. package/dist/native/config/PlatformHelper.web.js.map +1 -1
  53. package/dist/recyclerview/RecyclerView.d.ts +2 -1
  54. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  55. package/dist/recyclerview/RecyclerView.js +104 -57
  56. package/dist/recyclerview/RecyclerView.js.map +1 -1
  57. package/dist/recyclerview/RecyclerViewContextProvider.d.ts +41 -6
  58. package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -1
  59. package/dist/recyclerview/RecyclerViewContextProvider.js +4 -0
  60. package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -1
  61. package/dist/recyclerview/RecyclerViewManager.d.ts +24 -7
  62. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
  63. package/dist/recyclerview/RecyclerViewManager.js +119 -113
  64. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  65. package/dist/recyclerview/RenderStackManager.d.ts +86 -0
  66. package/dist/recyclerview/RenderStackManager.d.ts.map +1 -0
  67. package/dist/recyclerview/RenderStackManager.js +343 -0
  68. package/dist/recyclerview/RenderStackManager.js.map +1 -0
  69. package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
  70. package/dist/recyclerview/ViewHolder.js +5 -3
  71. package/dist/recyclerview/ViewHolder.js.map +1 -1
  72. package/dist/recyclerview/ViewHolderCollection.d.ts +9 -3
  73. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
  74. package/dist/recyclerview/ViewHolderCollection.js +26 -9
  75. package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
  76. package/dist/recyclerview/components/ScrollAnchor.d.ts +2 -2
  77. package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -1
  78. package/dist/recyclerview/components/ScrollAnchor.js +9 -5
  79. package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
  80. package/dist/recyclerview/components/StickyHeaders.d.ts +1 -1
  81. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
  82. package/dist/recyclerview/components/StickyHeaders.js +40 -33
  83. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  84. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts +45 -1
  85. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts.map +1 -1
  86. package/dist/recyclerview/helpers/EngagedIndicesTracker.js +77 -20
  87. package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -1
  88. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts +11 -0
  89. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts.map +1 -0
  90. package/dist/recyclerview/helpers/RenderTimeTracker.js +42 -0
  91. package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -0
  92. package/dist/recyclerview/helpers/VelocityTracker.d.ts +29 -0
  93. package/dist/recyclerview/helpers/VelocityTracker.d.ts.map +1 -0
  94. package/dist/recyclerview/helpers/VelocityTracker.js +70 -0
  95. package/dist/recyclerview/helpers/VelocityTracker.js.map +1 -0
  96. package/dist/recyclerview/hooks/useBoundDetection.d.ts +1 -2
  97. package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -1
  98. package/dist/recyclerview/hooks/useBoundDetection.js +56 -22
  99. package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
  100. package/dist/recyclerview/hooks/useLayoutState.d.ts +3 -1
  101. package/dist/recyclerview/hooks/useLayoutState.d.ts.map +1 -1
  102. package/dist/recyclerview/hooks/useLayoutState.js +5 -3
  103. package/dist/recyclerview/hooks/useLayoutState.js.map +1 -1
  104. package/dist/recyclerview/hooks/useMappingHelper.d.ts +1 -1
  105. package/dist/recyclerview/hooks/useMappingHelper.d.ts.map +1 -1
  106. package/dist/recyclerview/hooks/useMappingHelper.js +1 -1
  107. package/dist/recyclerview/hooks/useMappingHelper.js.map +1 -1
  108. package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -1
  109. package/dist/recyclerview/hooks/useOnLoad.js +4 -6
  110. package/dist/recyclerview/hooks/useOnLoad.js.map +1 -1
  111. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +5 -49
  112. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  113. package/dist/recyclerview/hooks/useRecyclerViewController.js +315 -204
  114. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  115. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts +2 -0
  116. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
  117. package/dist/recyclerview/hooks/useRecyclerViewManager.js +11 -1
  118. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
  119. package/dist/recyclerview/hooks/useRecyclingState.d.ts +4 -2
  120. package/dist/recyclerview/hooks/useRecyclingState.d.ts.map +1 -1
  121. package/dist/recyclerview/hooks/useRecyclingState.js +2 -2
  122. package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -1
  123. package/dist/recyclerview/hooks/useSecondaryProps.js +1 -1
  124. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts +10 -3
  125. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts.map +1 -1
  126. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js +33 -4
  127. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js.map +1 -1
  128. package/dist/recyclerview/hooks/useUnmountFlag.d.ts.map +1 -1
  129. package/dist/recyclerview/hooks/useUnmountFlag.js +1 -0
  130. package/dist/recyclerview/hooks/useUnmountFlag.js.map +1 -1
  131. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +18 -4
  132. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  133. package/dist/recyclerview/layout-managers/GridLayoutManager.js +60 -21
  134. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  135. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +35 -21
  136. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
  137. package/dist/recyclerview/layout-managers/LayoutManager.js +92 -28
  138. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
  139. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts +9 -1
  140. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -1
  141. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +28 -12
  142. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
  143. package/dist/recyclerview/utils/measureLayout.web.d.ts.map +1 -1
  144. package/dist/recyclerview/utils/measureLayout.web.js +1 -3
  145. package/dist/recyclerview/utils/measureLayout.web.js.map +1 -1
  146. package/dist/tsconfig.tsbuildinfo +1 -1
  147. package/dist/viewability/ViewToken.d.ts +2 -2
  148. package/dist/viewability/ViewToken.d.ts.map +1 -1
  149. package/dist/viewability/ViewabilityHelper.js +1 -1
  150. package/dist/viewability/ViewabilityHelper.js.map +1 -1
  151. package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
  152. package/dist/viewability/ViewabilityManager.js +11 -5
  153. package/dist/viewability/ViewabilityManager.js.map +1 -1
  154. package/jestSetup.js +30 -11
  155. package/package.json +2 -1
  156. package/src/AnimatedFlashList.ts +3 -2
  157. package/src/FlashList.tsx +24 -0
  158. package/src/FlashListProps.ts +41 -10
  159. package/src/FlashListRef.ts +320 -0
  160. package/src/MasonryFlashList.tsx +2 -2
  161. package/src/__tests__/RecyclerView.test.tsx +106 -31
  162. package/src/__tests__/RenderStackManager.test.ts +574 -0
  163. package/src/__tests__/helpers/createLayoutManager.ts +2 -3
  164. package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
  165. package/src/benchmark/useBenchmark.ts +0 -37
  166. package/src/benchmark/useFlatListBenchmark.ts +2 -2
  167. package/src/index.ts +2 -1
  168. package/src/native/config/PlatformHelper.android.ts +1 -0
  169. package/src/native/config/PlatformHelper.ios.ts +1 -0
  170. package/src/native/config/PlatformHelper.ts +1 -0
  171. package/src/native/config/PlatformHelper.web.ts +1 -0
  172. package/src/recyclerview/RecyclerView.tsx +139 -75
  173. package/src/recyclerview/RecyclerViewContextProvider.ts +52 -7
  174. package/src/recyclerview/RecyclerViewManager.ts +135 -98
  175. package/src/recyclerview/RenderStackManager.ts +317 -0
  176. package/src/recyclerview/ViewHolder.tsx +5 -3
  177. package/src/recyclerview/ViewHolderCollection.tsx +42 -14
  178. package/src/recyclerview/components/ScrollAnchor.tsx +21 -9
  179. package/src/recyclerview/components/StickyHeaders.tsx +63 -45
  180. package/src/recyclerview/helpers/EngagedIndicesTracker.ts +118 -23
  181. package/src/recyclerview/helpers/RenderTimeTracker.ts +42 -0
  182. package/src/recyclerview/helpers/VelocityTracker.ts +77 -0
  183. package/src/recyclerview/hooks/useBoundDetection.ts +72 -23
  184. package/src/recyclerview/hooks/useLayoutState.ts +15 -6
  185. package/src/recyclerview/hooks/useMappingHelper.ts +1 -1
  186. package/src/recyclerview/hooks/useOnLoad.ts +4 -6
  187. package/src/recyclerview/hooks/useRecyclerViewController.tsx +364 -254
  188. package/src/recyclerview/hooks/useRecyclerViewManager.ts +13 -1
  189. package/src/recyclerview/hooks/useRecyclingState.ts +11 -7
  190. package/src/recyclerview/hooks/useSecondaryProps.tsx +1 -1
  191. package/src/recyclerview/hooks/useUnmountAwareCallbacks.ts +39 -3
  192. package/src/recyclerview/hooks/useUnmountFlag.ts +1 -0
  193. package/src/recyclerview/layout-managers/GridLayoutManager.ts +67 -23
  194. package/src/recyclerview/layout-managers/LayoutManager.ts +110 -41
  195. package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +30 -8
  196. package/src/recyclerview/utils/measureLayout.web.ts +1 -3
  197. package/src/viewability/ViewToken.ts +2 -2
  198. package/src/viewability/ViewabilityHelper.ts +1 -1
  199. package/src/viewability/ViewabilityManager.ts +16 -9
  200. package/dist/__tests__/RecycleKeyManager.test.d.ts +0 -2
  201. package/dist/__tests__/RecycleKeyManager.test.d.ts.map +0 -1
  202. package/dist/__tests__/RecycleKeyManager.test.js +0 -210
  203. package/dist/__tests__/RecycleKeyManager.test.js.map +0 -1
  204. package/dist/recyclerview/RecycleKeyManager.d.ts +0 -82
  205. package/dist/recyclerview/RecycleKeyManager.d.ts.map +0 -1
  206. package/dist/recyclerview/RecycleKeyManager.js +0 -135
  207. package/dist/recyclerview/RecycleKeyManager.js.map +0 -1
  208. package/src/__tests__/RecycleKeyManager.test.ts +0 -254
  209. 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,104 +43,98 @@ 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.getDataLength());
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
- const pendingScrollResolves = useRef<(() => void)[]>([]);
108
60
 
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]);
61
+ // Queue to store callbacks that should be executed after scroll offset updates
62
+ const pendingScrollCallbacks = useRef<(() => void)[]>([]);
145
63
 
146
64
  // Handle initial scroll position when the list first loads
147
65
  // useOnLoad(recyclerViewManager, () => {
148
66
 
149
67
  // });
150
68
  /**
151
- * Updates the scroll offset and returns a Promise that resolves
152
- * when the update has been applied.
69
+ * Updates the scroll offset and calls the provided callback
70
+ * after the update has been applied and the component has re-rendered.
71
+ *
72
+ * @param offset - The new scroll offset to apply
73
+ * @param callback - Optional callback to execute after the update is applied
153
74
  */
154
- const updateScrollOffsetAsync = useCallback(
155
- async (offset: number): Promise<void> => {
156
- return new Promise((resolve) => {
157
- // TODO: Make sure we don't scroll beyond content size
158
- if (recyclerViewManager.updateScrollOffset(offset) !== undefined) {
159
- // Add the resolve function to the queue
160
- pendingScrollResolves.current.push(resolve);
161
- setRenderId((prev) => prev + 1);
162
- } else {
163
- resolve();
164
- }
165
- });
75
+ const updateScrollOffsetWithCallback = useCallback(
76
+ (offset: number, callback: () => void): void => {
77
+ // Attempt to update the scroll offset in the RecyclerViewManager
78
+ // This returns undefined if no update is needed
79
+ if (recyclerViewManager.updateScrollOffset(offset) !== undefined) {
80
+ // It will be executed after the next render
81
+ pendingScrollCallbacks.current.push(callback);
82
+ // Trigger a re-render to apply the scroll offset update
83
+ setRenderId((prev) => prev + 1);
84
+ } else {
85
+ // No update needed, execute callback immediately
86
+ callback();
87
+ }
166
88
  },
167
89
  [recyclerViewManager]
168
90
  );
169
91
 
92
+ const computeFirstVisibleIndexForOffsetCorrection = useCallback(() => {
93
+ const { data, keyExtractor } = recyclerViewManager.props;
94
+ if (
95
+ recyclerViewManager.getIsFirstLayoutComplete() &&
96
+ keyExtractor &&
97
+ recyclerViewManager.getDataLength() > 0 &&
98
+ recyclerViewManager.shouldMaintainVisibleContentPosition()
99
+ ) {
100
+ // Update the tracked first visible item
101
+ const firstVisibleIndex = Math.max(
102
+ 0,
103
+ recyclerViewManager.computeVisibleIndices().startIndex
104
+ );
105
+ if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
106
+ firstVisibleItemKey.current = keyExtractor(
107
+ data![firstVisibleIndex],
108
+ firstVisibleIndex
109
+ );
110
+ firstVisibleItemLayout.current = {
111
+ ...recyclerViewManager.getLayout(firstVisibleIndex),
112
+ };
113
+ }
114
+ }
115
+ }, [recyclerViewManager]);
116
+
170
117
  /**
171
118
  * Maintains the visible content position when the list updates.
172
119
  * This is particularly useful for chat applications where we want to keep
173
120
  * the user's current view position when new messages are added.
174
121
  */
175
- const applyContentOffset = useCallback(async () => {
176
- // Resolve all pending scroll updates from previous calls
177
- const resolves = pendingScrollResolves.current;
178
- pendingScrollResolves.current = [];
179
- resolves.forEach((resolve) => resolve());
122
+ const applyOffsetCorrection = useCallback(() => {
123
+ const { horizontal, data, keyExtractor } = recyclerViewManager.props;
124
+
125
+ // Execute all pending callbacks from previous scroll offset updates
126
+ // This ensures any scroll operations that were waiting for render are completed
127
+ const callbacks = pendingScrollCallbacks.current;
128
+ pendingScrollCallbacks.current = [];
129
+ callbacks.forEach((callback) => callback());
180
130
 
181
- const currentDataLength = props.data?.length ?? 0;
131
+ const currentDataLength = recyclerViewManager.getDataLength();
182
132
 
183
133
  if (
184
- !props.horizontal &&
185
134
  recyclerViewManager.getIsFirstLayoutComplete() &&
186
- props.keyExtractor &&
135
+ keyExtractor &&
187
136
  currentDataLength > 0 &&
188
- props.maintainVisibleContentPosition?.disabled !== true
137
+ recyclerViewManager.shouldMaintainVisibleContentPosition()
189
138
  ) {
190
139
  const hasDataChanged = currentDataLength !== lastDataLengthRef.current;
191
140
  // If we have a tracked first visible item, maintain its position
@@ -195,38 +144,54 @@ export function useRecyclerViewController<T>(
195
144
  .getEngagedIndices()
196
145
  .findValue(
197
146
  (index) =>
198
- props.keyExtractor?.(props.data![index], index) ===
147
+ keyExtractor?.(data![index], index) ===
199
148
  firstVisibleItemKey.current
200
149
  ) ??
201
150
  (hasDataChanged
202
- ? props.data?.findIndex(
151
+ ? data?.findIndex(
203
152
  (item, index) =>
204
- props.keyExtractor?.(item, index) ===
205
- firstVisibleItemKey.current
153
+ keyExtractor?.(item, index) === firstVisibleItemKey.current
206
154
  )
207
155
  : undefined);
208
156
 
209
- if (currentIndexOfFirstVisibleItem !== undefined) {
157
+ if (
158
+ currentIndexOfFirstVisibleItem !== undefined &&
159
+ currentIndexOfFirstVisibleItem >= 0
160
+ ) {
210
161
  // Calculate the difference in position and apply the offset
211
- const diff =
212
- recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem).y -
213
- firstVisibleItemLayout.current!.y;
162
+ const diff = horizontal
163
+ ? recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem).x -
164
+ firstVisibleItemLayout.current!.x
165
+ : recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem).y -
166
+ firstVisibleItemLayout.current!.y;
214
167
  firstVisibleItemLayout.current = {
215
168
  ...recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem),
216
169
  };
217
- if (diff !== 0 && !pauseAdjustRef.current) {
170
+ if (
171
+ diff !== 0 &&
172
+ !pauseOffsetCorrection.current &&
173
+ !recyclerViewManager.animationOptimizationsEnabled
174
+ ) {
218
175
  // console.log("diff", diff, firstVisibleItemKey.current);
219
176
  if (PlatformConfig.supportsOffsetCorrection) {
177
+ // console.log("scrollBy", diff);
220
178
  scrollAnchorRef.current?.scrollBy(diff);
221
179
  } else {
222
- scrollViewRef.current?.scrollTo({
223
- y: recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
224
- animated: false,
225
- });
180
+ const scrollToParams = horizontal
181
+ ? {
182
+ x: recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
183
+ animated: false,
184
+ }
185
+ : {
186
+ y: recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
187
+ animated: false,
188
+ };
189
+ scrollViewRef.current?.scrollTo(scrollToParams);
226
190
  }
227
191
  if (hasDataChanged) {
228
- updateScrollOffsetAsync(
229
- recyclerViewManager.getAbsoluteLastScrollOffset() + diff
192
+ updateScrollOffsetWithCallback(
193
+ recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
194
+ () => {}
230
195
  );
231
196
  recyclerViewManager.ignoreScrollEvents = true;
232
197
  setTimeout(() => {
@@ -237,27 +202,23 @@ export function useRecyclerViewController<T>(
237
202
  }
238
203
  }
239
204
 
240
- // Update the tracked first visible item
241
- const firstVisibleIndex = Math.max(
242
- 0,
243
- recyclerViewManager.getVisibleIndices().startIndex
244
- );
245
- if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
246
- firstVisibleItemKey.current = props.keyExtractor(
247
- props.data![firstVisibleIndex],
248
- firstVisibleIndex
249
- );
250
- firstVisibleItemLayout.current = {
251
- ...recyclerViewManager.getLayout(firstVisibleIndex),
252
- };
253
- }
205
+ computeFirstVisibleIndexForOffsetCorrection();
254
206
  }
255
- lastDataLengthRef.current = props.data?.length ?? 0;
256
- }, [props.data, props.keyExtractor, recyclerViewManager, setTimeout]);
257
-
258
- const handlerMethods = useMemo(() => {
207
+ lastDataLengthRef.current = recyclerViewManager.getDataLength();
208
+ }, [
209
+ recyclerViewManager,
210
+ scrollAnchorRef,
211
+ scrollViewRef,
212
+ setTimeout,
213
+ updateScrollOffsetWithCallback,
214
+ computeFirstVisibleIndexForOffsetCorrection,
215
+ ]);
216
+
217
+ const handlerMethods: FlashListRef<T> = useMemo(() => {
259
218
  return {
260
- props,
219
+ get props() {
220
+ return recyclerViewManager.props;
221
+ },
261
222
  /**
262
223
  * Scrolls the list to a specific offset position.
263
224
  * Handles RTL layouts and first item offset adjustments.
@@ -267,6 +228,7 @@ export function useRecyclerViewController<T>(
267
228
  animated,
268
229
  skipFirstItemOffset = true,
269
230
  }: ScrollToOffsetParams) => {
231
+ const { horizontal } = recyclerViewManager.props;
270
232
  if (scrollViewRef.current) {
271
233
  // Adjust offset for RTL layouts in horizontal mode
272
234
  if (I18nManager.isRTL && horizontal) {
@@ -295,6 +257,9 @@ export function useRecyclerViewController<T>(
295
257
  });
296
258
  }
297
259
  },
260
+ clearLayoutCacheOnUpdate: () => {
261
+ recyclerViewManager.markLayoutManagerDirty();
262
+ },
298
263
 
299
264
  // Expose native scroll view methods
300
265
  flashScrollIndicators: () => {
@@ -314,13 +279,16 @@ export function useRecyclerViewController<T>(
314
279
  * Scrolls to the end of the list.
315
280
  */
316
281
  scrollToEnd: async ({ animated }: ScrollToEdgeParams = {}) => {
282
+ const { data } = recyclerViewManager.props;
317
283
  if (data && data.length > 0) {
318
284
  await handlerMethods.scrollToIndex({
319
285
  index: data.length - 1,
320
286
  animated,
321
287
  });
322
288
  }
323
- scrollViewRef.current!.scrollToEnd({ animated });
289
+ setTimeout(() => {
290
+ scrollViewRef.current!.scrollToEnd({ animated });
291
+ }, 0);
324
292
  },
325
293
 
326
294
  /**
@@ -336,107 +304,190 @@ export function useRecyclerViewController<T>(
336
304
  /**
337
305
  * Scrolls to a specific index in the list.
338
306
  * Supports viewPosition and viewOffset for precise positioning.
307
+ * Returns a Promise that resolves when the scroll is complete.
339
308
  */
340
- scrollToIndex: async ({
309
+ scrollToIndex: ({
341
310
  index,
342
311
  animated,
343
312
  viewPosition,
344
313
  viewOffset,
345
- }: ScrollToIndexParams) => {
346
- if (scrollViewRef.current && data && data.length > index) {
347
- pauseAdjustRef.current = true;
348
-
349
- const getFinalOffset = () => {
350
- const layout = recyclerViewManager.getLayout(index);
351
- const offset = horizontal ? layout.x : layout.y;
352
- let finalOffset = offset;
353
- // take viewPosition etc into account
354
- if (viewPosition !== undefined || viewOffset !== undefined) {
355
- const containerSize = horizontal
356
- ? recyclerViewManager.getWindowSize().width
357
- : recyclerViewManager.getWindowSize().height;
358
-
359
- const itemSize = horizontal ? layout.width : layout.height;
360
-
361
- if (viewPosition !== undefined) {
362
- // viewPosition: 0 = top, 0.5 = center, 1 = bottom
363
- finalOffset =
364
- offset - (containerSize - itemSize) * viewPosition;
314
+ }: ScrollToIndexParams): Promise<void> => {
315
+ return new Promise((resolve) => {
316
+ const { horizontal } = recyclerViewManager.props;
317
+ if (
318
+ scrollViewRef.current &&
319
+ index >= 0 &&
320
+ index < recyclerViewManager.getDataLength()
321
+ ) {
322
+ // Pause the scroll offset adjustments
323
+ pauseOffsetCorrection.current = true;
324
+ recyclerViewManager.setOffsetProjectionEnabled(false);
325
+
326
+ const getFinalOffset = () => {
327
+ const layout = recyclerViewManager.getLayout(index);
328
+ const offset = horizontal ? layout.x : layout.y;
329
+ let finalOffset = offset;
330
+ // take viewPosition etc into account
331
+ if (viewPosition !== undefined || viewOffset !== undefined) {
332
+ const containerSize = horizontal
333
+ ? recyclerViewManager.getWindowSize().width
334
+ : recyclerViewManager.getWindowSize().height;
335
+
336
+ const itemSize = horizontal ? layout.width : layout.height;
337
+
338
+ if (viewPosition !== undefined) {
339
+ // viewPosition: 0 = top, 0.5 = center, 1 = bottom
340
+ finalOffset =
341
+ offset - (containerSize - itemSize) * viewPosition;
342
+ }
343
+
344
+ if (viewOffset !== undefined) {
345
+ finalOffset += viewOffset;
346
+ }
365
347
  }
366
-
367
- if (viewOffset !== undefined) {
368
- finalOffset += viewOffset;
348
+ return finalOffset + recyclerViewManager.firstItemOffset;
349
+ };
350
+ const lastAbsoluteScrollOffset =
351
+ recyclerViewManager.getAbsoluteLastScrollOffset();
352
+ const bufferForScroll = horizontal
353
+ ? recyclerViewManager.getWindowSize().width
354
+ : recyclerViewManager.getWindowSize().height;
355
+
356
+ const bufferForCompute = bufferForScroll * 2;
357
+
358
+ const getStartScrollOffset = () => {
359
+ let lastScrollOffset = lastAbsoluteScrollOffset;
360
+ const finalOffset = getFinalOffset();
361
+
362
+ if (finalOffset > lastScrollOffset) {
363
+ lastScrollOffset = Math.max(
364
+ finalOffset - bufferForCompute,
365
+ lastScrollOffset
366
+ );
367
+ recyclerViewManager.setScrollDirection("forward");
368
+ } else {
369
+ lastScrollOffset = Math.min(
370
+ finalOffset + bufferForCompute,
371
+ lastScrollOffset
372
+ );
373
+ recyclerViewManager.setScrollDirection("backward");
369
374
  }
370
- }
371
- return finalOffset;
372
- };
373
- let lastScrollOffset = recyclerViewManager.getLastScrollOffset();
374
- let finalOffset = getFinalOffset();
375
- const bufferForScroll = horizontal
376
- ? recyclerViewManager.getWindowSize().width
377
- : recyclerViewManager.getWindowSize().height;
378
-
379
- const bufferForCompute = bufferForScroll * 2;
380
-
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
- }
394
-
395
- if (animated) {
396
- // go from finalOffset to lastScrollOffset in 5 steps
397
- for (let i = 0; i < 5; i++) {
375
+ return lastScrollOffset;
376
+ };
377
+ let initialTargetOffset = getFinalOffset();
378
+ let initialStartScrollOffset = getStartScrollOffset();
379
+ let finalOffset = initialTargetOffset;
380
+ let startScrollOffset = initialStartScrollOffset;
381
+
382
+ const steps = 5;
383
+
384
+ /**
385
+ * Recursively performs the scroll animation steps.
386
+ * This function replaces the async/await loop with callback-based execution.
387
+ *
388
+ * @param currentStep - The current step in the animation (0 to steps-1)
389
+ */
390
+ const performScrollStep = (currentStep: number) => {
391
+ // Check if component is unmounted or we've completed all steps
398
392
  if (isUnmounted.current) {
393
+ resolve();
399
394
  return;
400
- }
401
- await updateScrollOffsetAsync(
402
- finalOffset + (lastScrollOffset - finalOffset) * (i / 4)
403
- );
404
- }
405
- } else {
406
- // go from lastScrollOffset to finalOffset in 5 steps
407
- for (let i = 0; i < 5; i++) {
408
- if (isUnmounted.current) {
395
+ } else if (currentStep >= steps) {
396
+ // All steps completed, perform final scroll
397
+ finishScrollToIndex();
409
398
  return;
410
399
  }
411
- await updateScrollOffsetAsync(
412
- lastScrollOffset + (finalOffset - lastScrollOffset) * (i / 4)
400
+
401
+ // Calculate the offset for this step
402
+ // For animated scrolls: interpolate from finalOffset to startScrollOffset
403
+ // For non-animated: interpolate from startScrollOffset to finalOffset
404
+ const nextOffset = animated
405
+ ? finalOffset +
406
+ (startScrollOffset - finalOffset) *
407
+ (currentStep / (steps - 1))
408
+ : startScrollOffset +
409
+ (finalOffset - startScrollOffset) *
410
+ (currentStep / (steps - 1));
411
+
412
+ // Update scroll offset with a callback to continue to the next step
413
+ updateScrollOffsetWithCallback(nextOffset, () => {
414
+ // Check if the index is still valid after the update
415
+ if (index >= recyclerViewManager.getDataLength()) {
416
+ // Index out of bounds, scroll to end instead
417
+ handlerMethods.scrollToEnd({ animated });
418
+ resolve(); // Resolve the promise as we're done
419
+ return;
420
+ }
421
+
422
+ // Check if the target position has changed significantly
423
+ const newFinalOffset = getFinalOffset();
424
+ if (
425
+ (newFinalOffset < initialTargetOffset &&
426
+ newFinalOffset < initialStartScrollOffset) ||
427
+ (newFinalOffset > initialTargetOffset &&
428
+ newFinalOffset > initialStartScrollOffset)
429
+ ) {
430
+ // Target has moved, recalculate and restart from beginning
431
+ finalOffset = newFinalOffset;
432
+ startScrollOffset = getStartScrollOffset();
433
+ initialTargetOffset = newFinalOffset;
434
+ initialStartScrollOffset = startScrollOffset;
435
+ performScrollStep(0); // Restart from step 0
436
+ } else {
437
+ // Continue to next step
438
+ performScrollStep(currentStep + 1);
439
+ }
440
+ });
441
+ };
442
+
443
+ /**
444
+ * Completes the scroll to index operation by performing the final scroll
445
+ * and re-enabling offset correction after a delay.
446
+ */
447
+ const finishScrollToIndex = () => {
448
+ finalOffset = getFinalOffset();
449
+ const maxOffset = recyclerViewManager.getMaxScrollOffset();
450
+
451
+ if (finalOffset > maxOffset) {
452
+ finalOffset = maxOffset;
453
+ }
454
+
455
+ if (animated) {
456
+ // For animated scrolls, first jump to the start position
457
+ // We don't need to add firstItemOffset here as it's already added
458
+ handlerMethods.scrollToOffset({
459
+ offset: startScrollOffset,
460
+ animated: false,
461
+ skipFirstItemOffset: true,
462
+ });
463
+ }
464
+
465
+ // Perform the final scroll to the target position
466
+ handlerMethods.scrollToOffset({
467
+ offset: finalOffset,
468
+ animated,
469
+ skipFirstItemOffset: true,
470
+ });
471
+
472
+ // Re-enable offset correction after a delay
473
+ // Longer delay for animated scrolls to allow animation to complete
474
+ setTimeout(
475
+ () => {
476
+ pauseOffsetCorrection.current = false;
477
+ recyclerViewManager.setOffsetProjectionEnabled(true);
478
+ resolve(); // Resolve the promise after re-enabling corrections
479
+ },
480
+ animated ? 300 : 200
413
481
  );
414
- }
415
- }
416
- finalOffset = getFinalOffset();
417
- const maxOffset = recyclerViewManager.getMaxScrollOffset();
482
+ };
418
483
 
419
- if (finalOffset > maxOffset) {
420
- finalOffset = maxOffset;
421
- }
422
- if (animated) {
423
- // We don't need to add firstItemOffset here as it will be added in scrollToOffset
424
- handlerMethods.scrollToOffset({
425
- offset: lastScrollOffset,
426
- animated: false,
427
- skipFirstItemOffset: false,
428
- });
484
+ // Start the scroll animation process
485
+ performScrollStep(0);
486
+ } else {
487
+ // Invalid parameters, resolve immediately
488
+ resolve();
429
489
  }
430
- handlerMethods.scrollToOffset({
431
- offset: finalOffset,
432
- animated,
433
- skipFirstItemOffset: false,
434
- });
435
-
436
- setTimeout(() => {
437
- pauseAdjustRef.current = false;
438
- }, 200);
439
- }
490
+ });
440
491
  },
441
492
 
442
493
  /**
@@ -449,11 +500,10 @@ export function useRecyclerViewController<T>(
449
500
  viewPosition,
450
501
  viewOffset,
451
502
  }: ScrollToItemParams<T>) => {
503
+ const { data } = recyclerViewManager.props;
452
504
  if (scrollViewRef.current && data) {
453
505
  // Find the index of the item in the data array
454
- const index = Array.from(data).findIndex(
455
- (dataItem) => dataItem === item
456
- );
506
+ const index = data.findIndex((dataItem) => dataItem === item);
457
507
  if (index >= 0) {
458
508
  handlerMethods.scrollToIndex({
459
509
  index,
@@ -473,7 +523,7 @@ export function useRecyclerViewController<T>(
473
523
  return recyclerViewManager.getWindowSize();
474
524
  },
475
525
  getLayout: (index: number) => {
476
- return recyclerViewManager.getLayout(index);
526
+ return recyclerViewManager.tryGetLayout(index);
477
527
  },
478
528
  getAbsoluteLastScrollOffset: () => {
479
529
  return recyclerViewManager.getAbsoluteLastScrollOffset();
@@ -484,11 +534,11 @@ export function useRecyclerViewController<T>(
484
534
  recordInteraction: () => {
485
535
  recyclerViewManager.recordInteraction();
486
536
  },
487
- getVisibleIndices: () => {
488
- return recyclerViewManager.getVisibleIndices();
537
+ computeVisibleIndices: () => {
538
+ return recyclerViewManager.computeVisibleIndices();
489
539
  },
490
540
  getFirstVisibleIndex: () => {
491
- return recyclerViewManager.getVisibleIndices().startIndex;
541
+ return recyclerViewManager.computeVisibleIndices().startIndex;
492
542
  },
493
543
  recomputeViewableItems: () => {
494
544
  recyclerViewManager.recomputeViewableItems();
@@ -497,19 +547,79 @@ export function useRecyclerViewController<T>(
497
547
  * Disables item recycling in preparation for layout animations.
498
548
  */
499
549
  prepareForLayoutAnimationRender: () => {
500
- recyclerViewManager.disableRecycling = true;
550
+ recyclerViewManager.animationOptimizationsEnabled = true;
501
551
  },
502
552
  };
503
- }, [horizontal, data, recyclerViewManager]);
553
+ }, [
554
+ recyclerViewManager,
555
+ scrollViewRef,
556
+ setTimeout,
557
+ isUnmounted,
558
+ updateScrollOffsetWithCallback,
559
+ ]);
560
+
561
+ const applyInitialScrollIndex = useCallback(() => {
562
+ const { horizontal, data } = recyclerViewManager.props;
563
+
564
+ const initialScrollIndex =
565
+ recyclerViewManager.getInitialScrollIndex() ?? -1;
566
+ const dataLength = data?.length ?? 0;
567
+ if (
568
+ initialScrollIndex >= 0 &&
569
+ initialScrollIndex < dataLength &&
570
+ !initialScrollCompletedRef.current &&
571
+ recyclerViewManager.getIsFirstLayoutComplete()
572
+ ) {
573
+ // Use setTimeout to ensure that we keep trying to scroll on first few renders
574
+ setTimeout(() => {
575
+ initialScrollCompletedRef.current = true;
576
+ pauseOffsetCorrection.current = false;
577
+ }, 100);
578
+
579
+ pauseOffsetCorrection.current = true;
580
+
581
+ const offset = horizontal
582
+ ? recyclerViewManager.getLayout(initialScrollIndex).x
583
+ : recyclerViewManager.getLayout(initialScrollIndex).y;
584
+ handlerMethods.scrollToOffset({
585
+ offset,
586
+ animated: false,
587
+ skipFirstItemOffset: false,
588
+ });
589
+
590
+ setTimeout(() => {
591
+ handlerMethods.scrollToOffset({
592
+ offset,
593
+ animated: false,
594
+ skipFirstItemOffset: false,
595
+ });
596
+ }, 0);
597
+ }
598
+ }, [handlerMethods, recyclerViewManager, setTimeout]);
504
599
 
505
600
  // Expose imperative methods through the ref
506
601
  useImperativeHandle(
507
602
  ref,
508
603
  () => {
509
- return { ...scrollViewRef.current, ...handlerMethods };
604
+ const imperativeApi = { ...scrollViewRef.current, ...handlerMethods };
605
+ // Without this the props getter from handlerMethods is evaluated during spread and
606
+ // future updates to props are not reflected in the ref
607
+ Object.defineProperty(imperativeApi, "props", {
608
+ get() {
609
+ return recyclerViewManager.props;
610
+ },
611
+ enumerable: true,
612
+ configurable: true,
613
+ });
614
+ return imperativeApi;
510
615
  },
511
- [handlerMethods]
616
+ [handlerMethods, scrollViewRef, recyclerViewManager]
512
617
  );
513
618
 
514
- return { applyContentOffset, applyInitialScrollIndex };
619
+ return {
620
+ applyOffsetCorrection,
621
+ computeFirstVisibleIndexForOffsetCorrection,
622
+ applyInitialScrollIndex,
623
+ handlerMethods,
624
+ };
515
625
  }