@shopify/flash-list 2.0.0-alpha.8 → 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 (197) hide show
  1. package/README.md +38 -98
  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 +63 -28
  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/enableNewCore.d.ts.map +1 -1
  31. package/dist/enableNewCore.js +2 -1
  32. package/dist/enableNewCore.js.map +1 -1
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/native/config/PlatformHelper.android.d.ts +2 -0
  37. package/dist/native/config/PlatformHelper.android.d.ts.map +1 -1
  38. package/dist/native/config/PlatformHelper.android.js +2 -0
  39. package/dist/native/config/PlatformHelper.android.js.map +1 -1
  40. package/dist/native/config/PlatformHelper.d.ts +2 -0
  41. package/dist/native/config/PlatformHelper.d.ts.map +1 -1
  42. package/dist/native/config/PlatformHelper.ios.d.ts +2 -0
  43. package/dist/native/config/PlatformHelper.ios.d.ts.map +1 -1
  44. package/dist/native/config/PlatformHelper.ios.js +2 -0
  45. package/dist/native/config/PlatformHelper.ios.js.map +1 -1
  46. package/dist/native/config/PlatformHelper.js +2 -0
  47. package/dist/native/config/PlatformHelper.js.map +1 -1
  48. package/dist/native/config/PlatformHelper.web.d.ts +2 -0
  49. package/dist/native/config/PlatformHelper.web.d.ts.map +1 -1
  50. package/dist/native/config/PlatformHelper.web.js +3 -1
  51. package/dist/native/config/PlatformHelper.web.js.map +1 -1
  52. package/dist/recyclerview/RecyclerView.d.ts +2 -1
  53. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  54. package/dist/recyclerview/RecyclerView.js +64 -37
  55. package/dist/recyclerview/RecyclerView.js.map +1 -1
  56. package/dist/recyclerview/RecyclerViewContextProvider.d.ts +6 -5
  57. package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -1
  58. package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -1
  59. package/dist/recyclerview/RecyclerViewManager.d.ts +21 -7
  60. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
  61. package/dist/recyclerview/RecyclerViewManager.js +105 -113
  62. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  63. package/dist/recyclerview/RenderStackManager.d.ts +85 -0
  64. package/dist/recyclerview/RenderStackManager.d.ts.map +1 -0
  65. package/dist/recyclerview/RenderStackManager.js +324 -0
  66. package/dist/recyclerview/RenderStackManager.js.map +1 -0
  67. package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
  68. package/dist/recyclerview/ViewHolder.js +5 -3
  69. package/dist/recyclerview/ViewHolder.js.map +1 -1
  70. package/dist/recyclerview/ViewHolderCollection.d.ts +3 -1
  71. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
  72. package/dist/recyclerview/ViewHolderCollection.js +23 -8
  73. package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
  74. package/dist/recyclerview/components/ScrollAnchor.d.ts +2 -1
  75. package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -1
  76. package/dist/recyclerview/components/ScrollAnchor.js +9 -4
  77. package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
  78. package/dist/recyclerview/components/StickyHeaders.d.ts +1 -1
  79. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
  80. package/dist/recyclerview/components/StickyHeaders.js +40 -32
  81. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  82. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts +45 -1
  83. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts.map +1 -1
  84. package/dist/recyclerview/helpers/EngagedIndicesTracker.js +79 -20
  85. package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -1
  86. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts +10 -0
  87. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts.map +1 -0
  88. package/dist/recyclerview/helpers/RenderTimeTracker.js +39 -0
  89. package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -0
  90. package/dist/recyclerview/helpers/VelocityTracker.d.ts +29 -0
  91. package/dist/recyclerview/helpers/VelocityTracker.d.ts.map +1 -0
  92. package/dist/recyclerview/helpers/VelocityTracker.js +70 -0
  93. package/dist/recyclerview/helpers/VelocityTracker.js.map +1 -0
  94. package/dist/recyclerview/hooks/useBoundDetection.d.ts +1 -2
  95. package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -1
  96. package/dist/recyclerview/hooks/useBoundDetection.js +19 -16
  97. package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
  98. package/dist/recyclerview/hooks/useMappingHelper.d.ts +1 -1
  99. package/dist/recyclerview/hooks/useMappingHelper.d.ts.map +1 -1
  100. package/dist/recyclerview/hooks/useMappingHelper.js +1 -1
  101. package/dist/recyclerview/hooks/useMappingHelper.js.map +1 -1
  102. package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -1
  103. package/dist/recyclerview/hooks/useOnLoad.js +4 -6
  104. package/dist/recyclerview/hooks/useOnLoad.js.map +1 -1
  105. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +3 -48
  106. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  107. package/dist/recyclerview/hooks/useRecyclerViewController.js +179 -119
  108. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  109. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts +2 -0
  110. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
  111. package/dist/recyclerview/hooks/useRecyclerViewManager.js +10 -1
  112. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
  113. package/dist/recyclerview/hooks/useSecondaryProps.js +1 -1
  114. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts +10 -3
  115. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts.map +1 -1
  116. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js +33 -4
  117. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js.map +1 -1
  118. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +6 -0
  119. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  120. package/dist/recyclerview/layout-managers/GridLayoutManager.js +27 -5
  121. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  122. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +10 -16
  123. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
  124. package/dist/recyclerview/layout-managers/LayoutManager.js +4 -14
  125. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
  126. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +2 -2
  127. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
  128. package/dist/recyclerview/utils/measureLayout.d.ts +1 -29
  129. package/dist/recyclerview/utils/measureLayout.d.ts.map +1 -1
  130. package/dist/recyclerview/utils/measureLayout.js +2 -4
  131. package/dist/recyclerview/utils/measureLayout.js.map +1 -1
  132. package/dist/recyclerview/utils/measureLayout.web.d.ts +29 -0
  133. package/dist/recyclerview/utils/measureLayout.web.d.ts.map +1 -0
  134. package/dist/recyclerview/utils/measureLayout.web.js +89 -0
  135. package/dist/recyclerview/utils/measureLayout.web.js.map +1 -0
  136. package/dist/tsconfig.tsbuildinfo +1 -1
  137. package/dist/viewability/ViewToken.d.ts +2 -2
  138. package/dist/viewability/ViewToken.d.ts.map +1 -1
  139. package/dist/viewability/ViewabilityHelper.js +1 -1
  140. package/dist/viewability/ViewabilityHelper.js.map +1 -1
  141. package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
  142. package/dist/viewability/ViewabilityManager.js +1 -2
  143. package/dist/viewability/ViewabilityManager.js.map +1 -1
  144. package/jestSetup.js +30 -11
  145. package/package.json +2 -1
  146. package/src/AnimatedFlashList.ts +3 -2
  147. package/src/FlashList.tsx +24 -0
  148. package/src/FlashListProps.ts +20 -8
  149. package/src/FlashListRef.ts +320 -0
  150. package/src/MasonryFlashList.tsx +2 -2
  151. package/src/__tests__/RecyclerView.test.tsx +84 -30
  152. package/src/__tests__/RenderStackManager.test.ts +575 -0
  153. package/src/__tests__/helpers/createLayoutManager.ts +2 -3
  154. package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
  155. package/src/benchmark/useFlatListBenchmark.ts +2 -2
  156. package/src/enableNewCore.ts +3 -1
  157. package/src/index.ts +1 -0
  158. package/src/native/config/PlatformHelper.android.ts +2 -0
  159. package/src/native/config/PlatformHelper.ios.ts +2 -0
  160. package/src/native/config/PlatformHelper.ts +2 -0
  161. package/src/native/config/PlatformHelper.web.ts +3 -1
  162. package/src/recyclerview/RecyclerView.tsx +85 -43
  163. package/src/recyclerview/RecyclerViewContextProvider.ts +12 -6
  164. package/src/recyclerview/RecyclerViewManager.ts +123 -98
  165. package/src/recyclerview/RenderStackManager.ts +291 -0
  166. package/src/recyclerview/ViewHolder.tsx +5 -3
  167. package/src/recyclerview/ViewHolderCollection.tsx +33 -12
  168. package/src/recyclerview/components/ScrollAnchor.tsx +21 -8
  169. package/src/recyclerview/components/StickyHeaders.tsx +63 -44
  170. package/src/recyclerview/helpers/EngagedIndicesTracker.ts +120 -23
  171. package/src/recyclerview/helpers/RenderTimeTracker.ts +38 -0
  172. package/src/recyclerview/helpers/VelocityTracker.ts +77 -0
  173. package/src/recyclerview/hooks/useBoundDetection.ts +25 -18
  174. package/src/recyclerview/hooks/useMappingHelper.ts +1 -1
  175. package/src/recyclerview/hooks/useOnLoad.ts +4 -6
  176. package/src/recyclerview/hooks/useRecyclerViewController.tsx +204 -173
  177. package/src/recyclerview/hooks/useRecyclerViewManager.ts +11 -1
  178. package/src/recyclerview/hooks/useSecondaryProps.tsx +1 -1
  179. package/src/recyclerview/hooks/useUnmountAwareCallbacks.ts +39 -3
  180. package/src/recyclerview/layout-managers/GridLayoutManager.ts +30 -7
  181. package/src/recyclerview/layout-managers/LayoutManager.ts +12 -21
  182. package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +1 -1
  183. package/src/recyclerview/utils/measureLayout.ts +3 -3
  184. package/src/recyclerview/utils/measureLayout.web.ts +104 -0
  185. package/src/viewability/ViewToken.ts +2 -2
  186. package/src/viewability/ViewabilityHelper.ts +1 -1
  187. package/src/viewability/ViewabilityManager.ts +6 -3
  188. package/dist/__tests__/RecycleKeyManager.test.d.ts +0 -2
  189. package/dist/__tests__/RecycleKeyManager.test.d.ts.map +0 -1
  190. package/dist/__tests__/RecycleKeyManager.test.js +0 -210
  191. package/dist/__tests__/RecycleKeyManager.test.js.map +0 -1
  192. package/dist/recyclerview/RecycleKeyManager.d.ts +0 -82
  193. package/dist/recyclerview/RecycleKeyManager.d.ts.map +0 -1
  194. package/dist/recyclerview/RecycleKeyManager.js +0 -135
  195. package/dist/recyclerview/RecycleKeyManager.js.map +0 -1
  196. package/src/__tests__/RecycleKeyManager.test.ts +0 -254
  197. package/src/recyclerview/RecycleKeyManager.ts +0 -185
@@ -8,66 +8,22 @@ 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";
15
21
  import { RVLayout } from "../layout-managers/LayoutManager";
16
22
  import { ScrollAnchorRef } from "../components/ScrollAnchor";
23
+ import { PlatformConfig } from "../../native/config/PlatformHelper";
17
24
 
18
25
  import { useUnmountFlag } from "./useUnmountFlag";
19
- import { useUnmountAwareCallbacks } from "./useUnmountAwareCallbacks";
20
-
21
- /**
22
- * Parameters for scrolling to a specific position in the list.
23
- * Extends ScrollToEdgeParams to include view positioning options.
24
- */
25
- export interface ScrollToParams extends ScrollToEdgeParams {
26
- /** Position of the target item relative to the viewport (0 = top, 0.5 = center, 1 = bottom) */
27
- viewPosition?: number;
28
- /** Additional offset to apply after viewPosition calculation */
29
- viewOffset?: number;
30
- }
31
-
32
- /**
33
- * Parameters for scrolling to a specific offset in the list.
34
- * Used when you want to scroll to an exact pixel position.
35
- */
36
- export interface ScrollToOffsetParams extends ScrollToParams {
37
- /** The pixel offset to scroll to */
38
- offset: number;
39
- /**
40
- * If true, the first item offset will not be added to the offset calculation.
41
- * First offset represents header size or top padding.
42
- */
43
- skipFirstItemOffset?: boolean;
44
- }
45
-
46
- /**
47
- * Parameters for scrolling to a specific index in the list.
48
- * Used when you want to scroll to a specific item by its position in the data array.
49
- */
50
- export interface ScrollToIndexParams extends ScrollToParams {
51
- /** The index of the item to scroll to */
52
- index: number;
53
- }
54
-
55
- /**
56
- * Parameters for scrolling to a specific item in the list.
57
- * Used when you want to scroll to a specific item by its data value.
58
- */
59
- export interface ScrollToItemParams<T> extends ScrollToParams {
60
- /** The item to scroll to */
61
- item: T;
62
- }
63
-
64
- /**
65
- * Base parameters for scrolling to the edges of the list.
66
- */
67
- export interface ScrollToEdgeParams {
68
- /** Whether the scroll should be animated */
69
- animated?: boolean;
70
- }
26
+ import { useUnmountAwareTimeout } from "./useUnmountAwareCallbacks";
71
27
 
72
28
  /**
73
29
  * Comprehensive hook that manages RecyclerView scrolling behavior and provides
@@ -87,61 +43,22 @@ export interface ScrollToEdgeParams {
87
43
  */
88
44
  export function useRecyclerViewController<T>(
89
45
  recyclerViewManager: RecyclerViewManager<T>,
90
- ref: React.Ref<any>,
46
+ ref: React.Ref<FlashListRef<T>>,
91
47
  scrollViewRef: RefObject<CompatScroller>,
92
- scrollAnchorRef: React.RefObject<ScrollAnchorRef>,
93
- props: RecyclerViewProps<T>
48
+ scrollAnchorRef: React.RefObject<ScrollAnchorRef>
94
49
  ) {
95
- const { horizontal, data } = props;
96
50
  const isUnmounted = useUnmountFlag();
97
51
  const [_, setRenderId] = useState(0);
98
- const pauseAdjustRef = useRef(false);
52
+ const pauseOffsetCorrection = useRef(false);
99
53
  const initialScrollCompletedRef = useRef(false);
100
- const lastDataLengthRef = useRef(data?.length ?? 0);
101
- const { setTimeout } = useUnmountAwareCallbacks();
54
+ const lastDataLengthRef = useRef(recyclerViewManager.props.data?.length ?? 0);
55
+ const { setTimeout } = useUnmountAwareTimeout();
102
56
 
103
57
  // Track the first visible item for maintaining scroll position
104
58
  const firstVisibleItemKey = useRef<string | undefined>(undefined);
105
59
  const firstVisibleItemLayout = useRef<RVLayout | undefined>(undefined);
106
60
  const pendingScrollResolves = useRef<(() => void)[]>([]);
107
61
 
108
- const applyInitialScrollIndex = useCallback(() => {
109
- const initialScrollIndex =
110
- recyclerViewManager.getInitialScrollIndex() ?? -1;
111
- const dataLength = props.data?.length ?? 0;
112
- if (
113
- initialScrollIndex >= 0 &&
114
- initialScrollIndex < dataLength &&
115
- !initialScrollCompletedRef.current &&
116
- recyclerViewManager.getIsFirstLayoutComplete()
117
- ) {
118
- // Use setTimeout to ensure that we keep trying to scroll on first few renders
119
- setTimeout(() => {
120
- initialScrollCompletedRef.current = true;
121
- pauseAdjustRef.current = false;
122
- }, 100);
123
-
124
- pauseAdjustRef.current = true;
125
-
126
- const offset = horizontal
127
- ? recyclerViewManager.getLayout(initialScrollIndex).x
128
- : recyclerViewManager.getLayout(initialScrollIndex).y;
129
- handlerMethods.scrollToOffset({
130
- offset,
131
- animated: false,
132
- skipFirstItemOffset: false,
133
- });
134
-
135
- setTimeout(() => {
136
- handlerMethods.scrollToOffset({
137
- offset,
138
- animated: false,
139
- skipFirstItemOffset: false,
140
- });
141
- }, 0);
142
- }
143
- }, [recyclerViewManager, props.data]);
144
-
145
62
  // Handle initial scroll position when the list first loads
146
63
  // useOnLoad(recyclerViewManager, () => {
147
64
 
@@ -172,19 +89,19 @@ export function useRecyclerViewController<T>(
172
89
  * the user's current view position when new messages are added.
173
90
  */
174
91
  const applyContentOffset = useCallback(async () => {
92
+ const { horizontal, data, keyExtractor } = recyclerViewManager.props;
175
93
  // Resolve all pending scroll updates from previous calls
176
94
  const resolves = pendingScrollResolves.current;
177
95
  pendingScrollResolves.current = [];
178
96
  resolves.forEach((resolve) => resolve());
179
97
 
180
- const currentDataLength = props.data?.length ?? 0;
98
+ const currentDataLength = data?.length ?? 0;
181
99
 
182
100
  if (
183
- !props.horizontal &&
184
101
  recyclerViewManager.getIsFirstLayoutComplete() &&
185
- props.keyExtractor &&
102
+ keyExtractor &&
186
103
  currentDataLength > 0 &&
187
- props.maintainVisibleContentPosition?.disabled !== true
104
+ recyclerViewManager.shouldMaintainVisibleContentPosition()
188
105
  ) {
189
106
  const hasDataChanged = currentDataLength !== lastDataLengthRef.current;
190
107
  // If we have a tracked first visible item, maintain its position
@@ -194,28 +111,46 @@ export function useRecyclerViewController<T>(
194
111
  .getEngagedIndices()
195
112
  .findValue(
196
113
  (index) =>
197
- props.keyExtractor?.(props.data![index], index) ===
114
+ keyExtractor?.(data![index], index) ===
198
115
  firstVisibleItemKey.current
199
116
  ) ??
200
117
  (hasDataChanged
201
- ? props.data?.findIndex(
118
+ ? data?.findIndex(
202
119
  (item, index) =>
203
- props.keyExtractor?.(item, index) ===
204
- firstVisibleItemKey.current
120
+ keyExtractor?.(item, index) === firstVisibleItemKey.current
205
121
  )
206
122
  : undefined);
207
123
 
208
- if (currentIndexOfFirstVisibleItem !== undefined) {
124
+ if (
125
+ currentIndexOfFirstVisibleItem !== undefined &&
126
+ currentIndexOfFirstVisibleItem >= 0
127
+ ) {
209
128
  // Calculate the difference in position and apply the offset
210
- const diff =
211
- recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem).y -
212
- 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;
213
134
  firstVisibleItemLayout.current = {
214
135
  ...recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem),
215
136
  };
216
- if (diff !== 0 && !pauseAdjustRef.current) {
137
+ if (diff !== 0 && !pauseOffsetCorrection.current) {
217
138
  // console.log("diff", diff, firstVisibleItemKey.current);
218
- scrollAnchorRef.current?.scrollBy(diff);
139
+ if (PlatformConfig.supportsOffsetCorrection) {
140
+ // console.log("scrollBy", diff);
141
+ scrollAnchorRef.current?.scrollBy(diff);
142
+ } else {
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);
153
+ }
219
154
  if (hasDataChanged) {
220
155
  updateScrollOffsetAsync(
221
156
  recyclerViewManager.getAbsoluteLastScrollOffset() + diff
@@ -232,11 +167,11 @@ export function useRecyclerViewController<T>(
232
167
  // Update the tracked first visible item
233
168
  const firstVisibleIndex = Math.max(
234
169
  0,
235
- recyclerViewManager.getVisibleIndices().startIndex
170
+ recyclerViewManager.computeVisibleIndices().startIndex
236
171
  );
237
172
  if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
238
- firstVisibleItemKey.current = props.keyExtractor(
239
- props.data![firstVisibleIndex],
173
+ firstVisibleItemKey.current = keyExtractor(
174
+ data![firstVisibleIndex],
240
175
  firstVisibleIndex
241
176
  );
242
177
  firstVisibleItemLayout.current = {
@@ -244,12 +179,20 @@ export function useRecyclerViewController<T>(
244
179
  };
245
180
  }
246
181
  }
247
- lastDataLengthRef.current = props.data?.length ?? 0;
248
- }, [props.data, props.keyExtractor, recyclerViewManager, setTimeout]);
249
-
250
- 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(() => {
251
192
  return {
252
- props,
193
+ get props() {
194
+ return recyclerViewManager.props;
195
+ },
253
196
  /**
254
197
  * Scrolls the list to a specific offset position.
255
198
  * Handles RTL layouts and first item offset adjustments.
@@ -259,6 +202,7 @@ export function useRecyclerViewController<T>(
259
202
  animated,
260
203
  skipFirstItemOffset = true,
261
204
  }: ScrollToOffsetParams) => {
205
+ const { horizontal } = recyclerViewManager.props;
262
206
  if (scrollViewRef.current) {
263
207
  // Adjust offset for RTL layouts in horizontal mode
264
208
  if (I18nManager.isRTL && horizontal) {
@@ -287,6 +231,9 @@ export function useRecyclerViewController<T>(
287
231
  });
288
232
  }
289
233
  },
234
+ clearLayoutCacheOnUpdate: () => {
235
+ recyclerViewManager.markLayoutManagerDirty();
236
+ },
290
237
 
291
238
  // Expose native scroll view methods
292
239
  flashScrollIndicators: () => {
@@ -306,13 +253,16 @@ export function useRecyclerViewController<T>(
306
253
  * Scrolls to the end of the list.
307
254
  */
308
255
  scrollToEnd: async ({ animated }: ScrollToEdgeParams = {}) => {
256
+ const { data } = recyclerViewManager.props;
309
257
  if (data && data.length > 0) {
310
258
  await handlerMethods.scrollToIndex({
311
259
  index: data.length - 1,
312
260
  animated,
313
261
  });
314
262
  }
315
- scrollViewRef.current!.scrollToEnd({ animated });
263
+ setTimeout(() => {
264
+ scrollViewRef.current!.scrollToEnd({ animated });
265
+ }, 0);
316
266
  },
317
267
 
318
268
  /**
@@ -335,8 +285,15 @@ export function useRecyclerViewController<T>(
335
285
  viewPosition,
336
286
  viewOffset,
337
287
  }: ScrollToIndexParams) => {
338
- if (scrollViewRef.current && data && data.length > index) {
339
- 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);
340
297
 
341
298
  const getFinalOffset = () => {
342
299
  const layout = recyclerViewManager.getLayout(index);
@@ -360,74 +317,104 @@ export function useRecyclerViewController<T>(
360
317
  finalOffset += viewOffset;
361
318
  }
362
319
  }
363
- return finalOffset;
320
+ return finalOffset + recyclerViewManager.firstItemOffset;
364
321
  };
365
- let lastScrollOffset = recyclerViewManager.getLastScrollOffset();
366
- let finalOffset = getFinalOffset();
322
+ const lastAbsoluteScrollOffset =
323
+ recyclerViewManager.getAbsoluteLastScrollOffset();
367
324
  const bufferForScroll = horizontal
368
325
  ? recyclerViewManager.getWindowSize().width
369
326
  : recyclerViewManager.getWindowSize().height;
370
327
 
371
328
  const bufferForCompute = bufferForScroll * 2;
372
329
 
373
- if (finalOffset > lastScrollOffset) {
374
- lastScrollOffset = Math.max(
375
- finalOffset - bufferForCompute,
376
- lastScrollOffset
377
- );
378
- recyclerViewManager.setScrollDirection("forward");
379
- } else {
380
- lastScrollOffset = Math.min(
381
- finalOffset + bufferForCompute,
382
- lastScrollOffset
383
- );
384
- recyclerViewManager.setScrollDirection("backward");
385
- }
330
+ const getStartScrollOffset = () => {
331
+ let lastScrollOffset = lastAbsoluteScrollOffset;
332
+ const finalOffset = getFinalOffset();
386
333
 
387
- if (animated) {
388
- // go from finalOffset to lastScrollOffset in 5 steps
389
- for (let i = 0; i < 5; i++) {
390
- if (isUnmounted.current) {
391
- return;
392
- }
393
- await updateScrollOffsetAsync(
394
- finalOffset + (lastScrollOffset - finalOffset) * (i / 4)
334
+ if (finalOffset > lastScrollOffset) {
335
+ lastScrollOffset = Math.max(
336
+ finalOffset - bufferForCompute,
337
+ lastScrollOffset
395
338
  );
396
- }
397
- } else {
398
- // go from lastScrollOffset to finalOffset in 5 steps
399
- for (let i = 0; i < 5; i++) {
400
- if (isUnmounted.current) {
401
- return;
402
- }
403
- await updateScrollOffsetAsync(
404
- lastScrollOffset + (finalOffset - lastScrollOffset) * (i / 4)
339
+ recyclerViewManager.setScrollDirection("forward");
340
+ } else {
341
+ lastScrollOffset = Math.min(
342
+ finalOffset + bufferForCompute,
343
+ lastScrollOffset
405
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
406
387
  }
407
388
  }
389
+
408
390
  finalOffset = getFinalOffset();
409
391
  const maxOffset = recyclerViewManager.getMaxScrollOffset();
410
392
 
411
393
  if (finalOffset > maxOffset) {
412
394
  finalOffset = maxOffset;
413
395
  }
396
+
414
397
  if (animated) {
415
- // 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
416
399
  handlerMethods.scrollToOffset({
417
- offset: lastScrollOffset,
400
+ offset: startScrollOffset,
418
401
  animated: false,
419
- skipFirstItemOffset: false,
402
+ skipFirstItemOffset: true,
420
403
  });
421
404
  }
422
405
  handlerMethods.scrollToOffset({
423
406
  offset: finalOffset,
424
407
  animated,
425
- skipFirstItemOffset: false,
408
+ skipFirstItemOffset: true,
426
409
  });
427
410
 
428
- setTimeout(() => {
429
- pauseAdjustRef.current = false;
430
- }, 200);
411
+ setTimeout(
412
+ () => {
413
+ pauseOffsetCorrection.current = false;
414
+ recyclerViewManager.setOffsetProjectionEnabled(true);
415
+ },
416
+ animated ? 300 : 200
417
+ );
431
418
  }
432
419
  },
433
420
 
@@ -441,11 +428,10 @@ export function useRecyclerViewController<T>(
441
428
  viewPosition,
442
429
  viewOffset,
443
430
  }: ScrollToItemParams<T>) => {
431
+ const { data } = recyclerViewManager.props;
444
432
  if (scrollViewRef.current && data) {
445
433
  // Find the index of the item in the data array
446
- const index = Array.from(data).findIndex(
447
- (dataItem) => dataItem === item
448
- );
434
+ const index = data.findIndex((dataItem) => dataItem === item);
449
435
  if (index >= 0) {
450
436
  handlerMethods.scrollToIndex({
451
437
  index,
@@ -465,7 +451,7 @@ export function useRecyclerViewController<T>(
465
451
  return recyclerViewManager.getWindowSize();
466
452
  },
467
453
  getLayout: (index: number) => {
468
- return recyclerViewManager.getLayout(index);
454
+ return recyclerViewManager.tryGetLayout(index);
469
455
  },
470
456
  getAbsoluteLastScrollOffset: () => {
471
457
  return recyclerViewManager.getAbsoluteLastScrollOffset();
@@ -476,11 +462,11 @@ export function useRecyclerViewController<T>(
476
462
  recordInteraction: () => {
477
463
  recyclerViewManager.recordInteraction();
478
464
  },
479
- getVisibleIndices: () => {
480
- return recyclerViewManager.getVisibleIndices();
465
+ computeVisibleIndices: () => {
466
+ return recyclerViewManager.computeVisibleIndices();
481
467
  },
482
468
  getFirstVisibleIndex: () => {
483
- return recyclerViewManager.getVisibleIndices().startIndex;
469
+ return recyclerViewManager.computeVisibleIndices().startIndex;
484
470
  },
485
471
  recomputeViewableItems: () => {
486
472
  recyclerViewManager.recomputeViewableItems();
@@ -489,10 +475,55 @@ export function useRecyclerViewController<T>(
489
475
  * Disables item recycling in preparation for layout animations.
490
476
  */
491
477
  prepareForLayoutAnimationRender: () => {
492
- recyclerViewManager.disableRecycling = true;
478
+ recyclerViewManager.disableRecycling(true);
493
479
  },
494
480
  };
495
- }, [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]);
496
527
 
497
528
  // Expose imperative methods through the ref
498
529
  useImperativeHandle(
@@ -500,8 +531,8 @@ export function useRecyclerViewController<T>(
500
531
  () => {
501
532
  return { ...scrollViewRef.current, ...handlerMethods };
502
533
  },
503
- [handlerMethods]
534
+ [handlerMethods, scrollViewRef]
504
535
  );
505
536
 
506
- return { applyContentOffset, applyInitialScrollIndex };
537
+ return { applyContentOffset, applyInitialScrollIndex, handlerMethods };
507
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
+ }