@shopify/flash-list 1.8.0 → 2.0.0-alpha.2

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 (170) hide show
  1. package/README.md +147 -26
  2. package/dist/FlashListProps.d.ts +65 -2
  3. package/dist/FlashListProps.d.ts.map +1 -1
  4. package/dist/__tests__/AverageWindow.test.js +35 -0
  5. package/dist/__tests__/AverageWindow.test.js.map +1 -1
  6. package/dist/enableNewCore.d.ts +3 -0
  7. package/dist/enableNewCore.d.ts.map +1 -0
  8. package/dist/enableNewCore.js +25 -0
  9. package/dist/enableNewCore.js.map +1 -0
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +28 -8
  13. package/dist/index.js.map +1 -1
  14. package/dist/recyclerview/RecycleKeyManager.d.ts +82 -0
  15. package/dist/recyclerview/RecycleKeyManager.d.ts.map +1 -0
  16. package/dist/recyclerview/RecycleKeyManager.js +135 -0
  17. package/dist/recyclerview/RecycleKeyManager.js.map +1 -0
  18. package/dist/recyclerview/RecyclerView.d.ts +12 -0
  19. package/dist/recyclerview/RecyclerView.d.ts.map +1 -0
  20. package/dist/recyclerview/RecyclerView.js +283 -0
  21. package/dist/recyclerview/RecyclerView.js.map +1 -0
  22. package/dist/recyclerview/RecyclerViewContextProvider.d.ts +12 -0
  23. package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -0
  24. package/dist/recyclerview/RecyclerViewContextProvider.js +11 -0
  25. package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -0
  26. package/dist/recyclerview/RecyclerViewManager.d.ts +52 -0
  27. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -0
  28. package/dist/recyclerview/RecyclerViewManager.js +323 -0
  29. package/dist/recyclerview/RecyclerViewManager.js.map +1 -0
  30. package/dist/recyclerview/RecyclerViewProps.d.ts +9 -0
  31. package/dist/recyclerview/RecyclerViewProps.d.ts.map +1 -0
  32. package/dist/recyclerview/RecyclerViewProps.js +3 -0
  33. package/dist/recyclerview/RecyclerViewProps.js.map +1 -0
  34. package/dist/recyclerview/ViewHolder.d.ts +45 -0
  35. package/dist/recyclerview/ViewHolder.d.ts.map +1 -0
  36. package/dist/recyclerview/ViewHolder.js +96 -0
  37. package/dist/recyclerview/ViewHolder.js.map +1 -0
  38. package/dist/recyclerview/ViewHolderCollection.d.ts +57 -0
  39. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -0
  40. package/dist/recyclerview/ViewHolderCollection.js +75 -0
  41. package/dist/recyclerview/ViewHolderCollection.js.map +1 -0
  42. package/dist/recyclerview/components/CompatScroller.d.ts +7 -0
  43. package/dist/recyclerview/components/CompatScroller.d.ts.map +1 -0
  44. package/dist/recyclerview/components/CompatScroller.js +8 -0
  45. package/dist/recyclerview/components/CompatScroller.js.map +1 -0
  46. package/dist/recyclerview/components/CompatView.d.ts +7 -0
  47. package/dist/recyclerview/components/CompatView.d.ts.map +1 -0
  48. package/dist/recyclerview/components/CompatView.js +8 -0
  49. package/dist/recyclerview/components/CompatView.js.map +1 -0
  50. package/dist/recyclerview/components/ScrollAnchor.d.ts +28 -0
  51. package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -0
  52. package/dist/recyclerview/components/ScrollAnchor.js +35 -0
  53. package/dist/recyclerview/components/ScrollAnchor.js.map +1 -0
  54. package/dist/recyclerview/components/StickyHeaders.d.ts +38 -0
  55. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -0
  56. package/dist/recyclerview/components/StickyHeaders.js +119 -0
  57. package/dist/recyclerview/components/StickyHeaders.js.map +1 -0
  58. package/dist/recyclerview/helpers/ConsecutiveNumbers.d.ts +51 -0
  59. package/dist/recyclerview/helpers/ConsecutiveNumbers.d.ts.map +1 -0
  60. package/dist/recyclerview/helpers/ConsecutiveNumbers.js +122 -0
  61. package/dist/recyclerview/helpers/ConsecutiveNumbers.js.map +1 -0
  62. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts +59 -0
  63. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts.map +1 -0
  64. package/dist/recyclerview/helpers/EngagedIndicesTracker.js +138 -0
  65. package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -0
  66. package/dist/recyclerview/hooks/useBoundDetection.d.ts +19 -0
  67. package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -0
  68. package/dist/recyclerview/hooks/useBoundDetection.js +103 -0
  69. package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -0
  70. package/dist/recyclerview/hooks/useLayoutState.d.ts +12 -0
  71. package/dist/recyclerview/hooks/useLayoutState.d.ts.map +1 -0
  72. package/dist/recyclerview/hooks/useLayoutState.js +43 -0
  73. package/dist/recyclerview/hooks/useLayoutState.js.map +1 -0
  74. package/dist/recyclerview/hooks/useOnLoad.d.ts +25 -0
  75. package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -0
  76. package/dist/recyclerview/hooks/useOnLoad.js +73 -0
  77. package/dist/recyclerview/hooks/useOnLoad.js.map +1 -0
  78. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +72 -0
  79. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -0
  80. package/dist/recyclerview/hooks/useRecyclerViewController.js +370 -0
  81. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -0
  82. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts +6 -0
  83. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -0
  84. package/dist/recyclerview/hooks/useRecyclerViewManager.js +27 -0
  85. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -0
  86. package/dist/recyclerview/hooks/useRecyclingState.d.ts +16 -0
  87. package/dist/recyclerview/hooks/useRecyclingState.d.ts.map +1 -0
  88. package/dist/recyclerview/hooks/useRecyclingState.js +54 -0
  89. package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -0
  90. package/dist/recyclerview/hooks/useSecondaryProps.d.ts +27 -0
  91. package/dist/recyclerview/hooks/useSecondaryProps.d.ts.map +1 -0
  92. package/dist/recyclerview/hooks/useSecondaryProps.js +93 -0
  93. package/dist/recyclerview/hooks/useSecondaryProps.js.map +1 -0
  94. package/dist/recyclerview/hooks/useUnmountFlag.d.ts +11 -0
  95. package/dist/recyclerview/hooks/useUnmountFlag.d.ts.map +1 -0
  96. package/dist/recyclerview/hooks/useUnmountFlag.js +28 -0
  97. package/dist/recyclerview/hooks/useUnmountFlag.js.map +1 -0
  98. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +65 -0
  99. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -0
  100. package/dist/recyclerview/layout-managers/GridLayoutManager.js +204 -0
  101. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -0
  102. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +281 -0
  103. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -0
  104. package/dist/recyclerview/layout-managers/LayoutManager.js +250 -0
  105. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -0
  106. package/dist/recyclerview/layout-managers/LinearLayoutManager.d.ts +52 -0
  107. package/dist/recyclerview/layout-managers/LinearLayoutManager.d.ts.map +1 -0
  108. package/dist/recyclerview/layout-managers/LinearLayoutManager.js +191 -0
  109. package/dist/recyclerview/layout-managers/LinearLayoutManager.js.map +1 -0
  110. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts +73 -0
  111. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -0
  112. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +274 -0
  113. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -0
  114. package/dist/recyclerview/utils/adjustOffsetForRTL.d.ts +12 -0
  115. package/dist/recyclerview/utils/adjustOffsetForRTL.d.ts.map +1 -0
  116. package/dist/recyclerview/utils/adjustOffsetForRTL.js +18 -0
  117. package/dist/recyclerview/utils/adjustOffsetForRTL.js.map +1 -0
  118. package/dist/recyclerview/utils/componentUtils.d.ts +19 -0
  119. package/dist/recyclerview/utils/componentUtils.d.ts.map +1 -0
  120. package/dist/recyclerview/utils/componentUtils.js +32 -0
  121. package/dist/recyclerview/utils/componentUtils.js.map +1 -0
  122. package/dist/recyclerview/utils/findVisibleIndex.d.ts +24 -0
  123. package/dist/recyclerview/utils/findVisibleIndex.d.ts.map +1 -0
  124. package/dist/recyclerview/utils/findVisibleIndex.js +82 -0
  125. package/dist/recyclerview/utils/findVisibleIndex.js.map +1 -0
  126. package/dist/recyclerview/utils/measureLayout.d.ts +56 -0
  127. package/dist/recyclerview/utils/measureLayout.d.ts.map +1 -0
  128. package/dist/recyclerview/utils/measureLayout.js +77 -0
  129. package/dist/recyclerview/utils/measureLayout.js.map +1 -0
  130. package/dist/tsconfig.tsbuildinfo +1 -1
  131. package/dist/utils/AverageWindow.d.ts +13 -0
  132. package/dist/utils/AverageWindow.d.ts.map +1 -1
  133. package/dist/utils/AverageWindow.js +30 -1
  134. package/dist/utils/AverageWindow.js.map +1 -1
  135. package/package.json +1 -1
  136. package/src/FlashListProps.ts +73 -2
  137. package/src/__tests__/AverageWindow.test.ts +49 -1
  138. package/src/enableNewCore.ts +22 -0
  139. package/src/index.ts +21 -0
  140. package/src/recyclerview/RecycleKeyManager.ts +185 -0
  141. package/src/recyclerview/RecyclerView.tsx +500 -0
  142. package/src/recyclerview/RecyclerViewContextProvider.ts +19 -0
  143. package/src/recyclerview/RecyclerViewManager.ts +379 -0
  144. package/src/recyclerview/RecyclerViewProps.ts +10 -0
  145. package/src/recyclerview/ViewHolder.tsx +173 -0
  146. package/src/recyclerview/ViewHolderCollection.tsx +164 -0
  147. package/src/recyclerview/components/CompatScroller.ts +9 -0
  148. package/src/recyclerview/components/CompatView.ts +9 -0
  149. package/src/recyclerview/components/ScrollAnchor.tsx +53 -0
  150. package/src/recyclerview/components/StickyHeaders.tsx +210 -0
  151. package/src/recyclerview/helpers/ConsecutiveNumbers.ts +120 -0
  152. package/src/recyclerview/helpers/EngagedIndicesTracker.ts +191 -0
  153. package/src/recyclerview/hooks/useBoundDetection.ts +127 -0
  154. package/src/recyclerview/hooks/useLayoutState.ts +46 -0
  155. package/src/recyclerview/hooks/useOnLoad.ts +78 -0
  156. package/src/recyclerview/hooks/useRecyclerViewController.tsx +487 -0
  157. package/src/recyclerview/hooks/useRecyclerViewManager.ts +30 -0
  158. package/src/recyclerview/hooks/useRecyclingState.ts +63 -0
  159. package/src/recyclerview/hooks/useSecondaryProps.tsx +119 -0
  160. package/src/recyclerview/hooks/useUnmountFlag.ts +26 -0
  161. package/src/recyclerview/layout-managers/GridLayoutManager.ts +215 -0
  162. package/src/recyclerview/layout-managers/LayoutManager.ts +493 -0
  163. package/src/recyclerview/layout-managers/LinearLayoutManager.ts +167 -0
  164. package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +302 -0
  165. package/src/recyclerview/utils/adjustOffsetForRTL.ts +17 -0
  166. package/src/recyclerview/utils/componentUtils.ts +28 -0
  167. package/src/recyclerview/utils/findVisibleIndex.ts +94 -0
  168. package/src/recyclerview/utils/measureLayout.ts +89 -0
  169. package/src/utils/AverageWindow.ts +33 -0
  170. package/src/viewability/ViewToken.ts +1 -1
@@ -0,0 +1,487 @@
1
+ import {
2
+ RefObject,
3
+ useCallback,
4
+ useImperativeHandle,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+ import { I18nManager } from "react-native";
10
+ import { RecyclerViewProps } from "../RecyclerViewProps";
11
+ import { CompatScroller } from "../components/CompatScroller";
12
+ import { RecyclerViewManager } from "../RecyclerViewManager";
13
+ import { adjustOffsetForRTL } from "../utils/adjustOffsetForRTL";
14
+ import { useUnmountFlag } from "./useUnmountFlag";
15
+ import { RVLayout } from "../layout-managers/LayoutManager";
16
+ import { ScrollAnchorRef } from "../components/ScrollAnchor";
17
+
18
+ /**
19
+ * Parameters for scrolling to a specific position in the list.
20
+ * Extends ScrollToEdgeParams to include view positioning options.
21
+ */
22
+ export interface ScrollToParams extends ScrollToEdgeParams {
23
+ /** Position of the target item relative to the viewport (0 = top, 0.5 = center, 1 = bottom) */
24
+ viewPosition?: number;
25
+ /** Additional offset to apply after viewPosition calculation */
26
+ viewOffset?: number;
27
+ }
28
+
29
+ /**
30
+ * Parameters for scrolling to a specific offset in the list.
31
+ * Used when you want to scroll to an exact pixel position.
32
+ */
33
+ export interface ScrollToOffsetParams extends ScrollToParams {
34
+ /** The pixel offset to scroll to */
35
+ offset: number;
36
+ /**
37
+ * If true, the first item offset will not be added to the offset calculation.
38
+ * First offset represents header size or top padding.
39
+ */
40
+ skipFirstItemOffset?: boolean;
41
+ }
42
+
43
+ /**
44
+ * Parameters for scrolling to a specific index in the list.
45
+ * Used when you want to scroll to a specific item by its position in the data array.
46
+ */
47
+ export interface ScrollToIndexParams extends ScrollToParams {
48
+ /** The index of the item to scroll to */
49
+ index: number;
50
+ }
51
+
52
+ /**
53
+ * Parameters for scrolling to a specific item in the list.
54
+ * Used when you want to scroll to a specific item by its data value.
55
+ */
56
+ export interface ScrollToItemParams<T> extends ScrollToParams {
57
+ /** The item to scroll to */
58
+ item: T;
59
+ }
60
+
61
+ /**
62
+ * Base parameters for scrolling to the edges of the list.
63
+ */
64
+ export interface ScrollToEdgeParams {
65
+ /** Whether the scroll should be animated */
66
+ animated?: boolean;
67
+ }
68
+
69
+ /**
70
+ * Comprehensive hook that manages RecyclerView scrolling behavior and provides
71
+ * imperative methods for controlling the RecyclerView.
72
+ *
73
+ * This hook combines content offset management and scroll handling functionality:
74
+ * 1. Provides imperative methods for scrolling and measurement
75
+ * 2. Handles initial scroll position when the list first loads
76
+ * 3. Maintains visible content position during updates
77
+ * 4. Manages scroll anchors for chat-like applications
78
+ *
79
+ * @param recyclerViewManager - The RecyclerViewManager instance that handles core functionality
80
+ * @param ref - The ref to expose the imperative methods
81
+ * @param scrollViewRef - Reference to the scrollable container component
82
+ * @param scrollAnchorRef - Reference to the scroll anchor component
83
+ * @param props - The RecyclerViewProps containing configuration
84
+ */
85
+ export function useRecyclerViewController<T>(
86
+ recyclerViewManager: RecyclerViewManager<T>,
87
+ ref: React.Ref<any>,
88
+ scrollViewRef: RefObject<CompatScroller>,
89
+ scrollAnchorRef: React.RefObject<ScrollAnchorRef>,
90
+ props: RecyclerViewProps<T>
91
+ ) {
92
+ const { horizontal, data } = props;
93
+ const isUnmounted = useUnmountFlag();
94
+ const [_, setRenderId] = useState(0);
95
+ const pauseAdjustRef = useRef(false);
96
+ const initialScrollCompletedRef = useRef(false);
97
+
98
+ // Track the first visible item for maintaining scroll position
99
+ const firstVisibleItemKey = useRef<string | undefined>(undefined);
100
+ const firstVisibleItemLayout = useRef<RVLayout | undefined>(undefined);
101
+ const pendingScrollResolves = useRef<(() => void)[]>([]);
102
+
103
+ const applyInitialScrollIndex = useCallback(() => {
104
+ const initialScrollIndex =
105
+ recyclerViewManager.getInitialScrollIndex() ?? -1;
106
+ const dataLength = props.data?.length ?? 0;
107
+ if (
108
+ initialScrollIndex >= 0 &&
109
+ initialScrollIndex < dataLength &&
110
+ !initialScrollCompletedRef.current &&
111
+ recyclerViewManager.getIsFirstLayoutComplete()
112
+ ) {
113
+ // Use setTimeout to ensure that we keep trying to scroll on first few renders
114
+ setTimeout(() => {
115
+ initialScrollCompletedRef.current = true;
116
+ pauseAdjustRef.current = false;
117
+ }, 100);
118
+
119
+ pauseAdjustRef.current = true;
120
+
121
+ const offset = horizontal
122
+ ? recyclerViewManager.getLayout(initialScrollIndex).x
123
+ : recyclerViewManager.getLayout(initialScrollIndex).y;
124
+ handlerMethods.scrollToOffset({
125
+ offset,
126
+ animated: false,
127
+ });
128
+
129
+ setTimeout(() => {
130
+ handlerMethods.scrollToOffset({
131
+ offset,
132
+ animated: false,
133
+ });
134
+ }, 0);
135
+ }
136
+ }, [recyclerViewManager, props.data]);
137
+
138
+ // Handle initial scroll position when the list first loads
139
+ // useOnLoad(recyclerViewManager, () => {
140
+
141
+ // });
142
+ /**
143
+ * Updates the scroll offset and returns a Promise that resolves
144
+ * when the update has been applied.
145
+ */
146
+ const updateScrollOffsetAsync = useCallback(
147
+ async (offset: number): Promise<void> => {
148
+ return new Promise((resolve) => {
149
+ recyclerViewManager.updateScrollOffset(offset);
150
+ // Add the resolve function to the queue
151
+ pendingScrollResolves.current.push(resolve);
152
+ setRenderId((prev) => prev + 1);
153
+ });
154
+ },
155
+ [recyclerViewManager]
156
+ );
157
+
158
+ /**
159
+ * Maintains the visible content position when the list updates.
160
+ * This is particularly useful for chat applications where we want to keep
161
+ * the user's current view position when new messages are added.
162
+ */
163
+ const applyContentOffset = useCallback(async () => {
164
+ // Resolve all pending scroll updates from previous calls
165
+ const resolves = pendingScrollResolves.current;
166
+ pendingScrollResolves.current = [];
167
+ resolves.forEach((resolve) => resolve());
168
+
169
+ if (
170
+ !props.horizontal &&
171
+ recyclerViewManager.getIsFirstLayoutComplete() &&
172
+ props.keyExtractor &&
173
+ props.maintainVisibleContentPosition?.disabled !== true
174
+ ) {
175
+ // If we have a tracked first visible item, maintain its position
176
+ if (firstVisibleItemKey.current) {
177
+ const currentIndexOfFirstVisibleItem = recyclerViewManager
178
+ .getEngagedIndices()
179
+ .findValue(
180
+ (index) =>
181
+ props.keyExtractor?.(props.data![index], index) ===
182
+ firstVisibleItemKey.current
183
+ );
184
+
185
+ if (currentIndexOfFirstVisibleItem !== undefined) {
186
+ // Calculate the difference in position and apply the offset
187
+ const diff =
188
+ recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem).y -
189
+ firstVisibleItemLayout.current!.y;
190
+ firstVisibleItemLayout.current = {
191
+ ...recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem),
192
+ };
193
+ if (diff !== 0 && !pauseAdjustRef.current) {
194
+ //console.log("diff", diff, firstVisibleItemKey.current);
195
+ scrollAnchorRef.current?.scrollBy(diff);
196
+ }
197
+ }
198
+ }
199
+
200
+ // Update the tracked first visible item
201
+ const firstVisibleIndex =
202
+ recyclerViewManager.getVisibleIndices().startIndex;
203
+ if (firstVisibleIndex !== undefined) {
204
+ firstVisibleItemKey.current =
205
+ props.keyExtractor?.(
206
+ props.data![firstVisibleIndex],
207
+ firstVisibleIndex
208
+ ) ?? "0";
209
+ firstVisibleItemLayout.current = {
210
+ ...recyclerViewManager.getLayout(firstVisibleIndex),
211
+ };
212
+ }
213
+ }
214
+ }, [props.data, props.keyExtractor, recyclerViewManager]);
215
+
216
+ const handlerMethods = useMemo(() => {
217
+ return {
218
+ props,
219
+ /**
220
+ * Scrolls the list to a specific offset position.
221
+ * Handles RTL layouts and first item offset adjustments.
222
+ */
223
+ scrollToOffset: ({
224
+ offset,
225
+ animated,
226
+ skipFirstItemOffset = true,
227
+ }: ScrollToOffsetParams) => {
228
+ if (scrollViewRef.current) {
229
+ // Adjust offset for RTL layouts in horizontal mode
230
+ if (I18nManager.isRTL && horizontal) {
231
+ offset = adjustOffsetForRTL(
232
+ offset,
233
+ recyclerViewManager.getChildContainerDimensions().width,
234
+ recyclerViewManager.getWindowSize().width
235
+ );
236
+ }
237
+
238
+ // Calculate the final offset including first item offset if needed
239
+ const adjustedOffset =
240
+ offset +
241
+ (skipFirstItemOffset ? 0 : recyclerViewManager.firstItemOffset);
242
+ const scrollTo = horizontal
243
+ ? { x: adjustedOffset, y: 0 }
244
+ : { x: 0, y: adjustedOffset };
245
+ scrollViewRef.current.scrollTo({
246
+ ...scrollTo,
247
+ animated,
248
+ });
249
+ }
250
+ },
251
+
252
+ // Expose native scroll view methods
253
+ flashScrollIndicators: () => {
254
+ scrollViewRef.current!.flashScrollIndicators();
255
+ },
256
+ getNativeScrollRef: () => {
257
+ return scrollViewRef.current;
258
+ },
259
+ getScrollResponder: () => {
260
+ return scrollViewRef.current!.getScrollResponder();
261
+ },
262
+ getScrollableNode: () => {
263
+ return scrollViewRef.current!.getScrollableNode();
264
+ },
265
+
266
+ /**
267
+ * Scrolls to the end of the list.
268
+ */
269
+ scrollToEnd: async ({ animated }: ScrollToEdgeParams = {}) => {
270
+ if (data && data.length > 0) {
271
+ await handlerMethods.scrollToIndex({
272
+ index: data.length - 1,
273
+ animated,
274
+ });
275
+ }
276
+ scrollViewRef.current!.scrollToEnd({ animated });
277
+ },
278
+
279
+ /**
280
+ * Scrolls to the beginning of the list.
281
+ */
282
+ scrollToTop: ({ animated }: ScrollToEdgeParams = {}) => {
283
+ handlerMethods.scrollToOffset({
284
+ offset: 0,
285
+ animated,
286
+ });
287
+ },
288
+
289
+ /**
290
+ * Scrolls to a specific index in the list.
291
+ * Supports viewPosition and viewOffset for precise positioning.
292
+ */
293
+ scrollToIndex: async ({
294
+ index,
295
+ animated,
296
+ viewPosition,
297
+ viewOffset,
298
+ }: ScrollToIndexParams) => {
299
+ if (scrollViewRef.current && data && data.length > index) {
300
+ pauseAdjustRef.current = true;
301
+ const layout = recyclerViewManager.getLayout(index);
302
+ let lastScrollOffset = recyclerViewManager.getLastScrollOffset();
303
+ const bufferForScroll = horizontal
304
+ ? recyclerViewManager.getWindowSize().width
305
+ : recyclerViewManager.getWindowSize().height;
306
+
307
+ const bufferForCompute = bufferForScroll * 2;
308
+
309
+ if (layout) {
310
+ let prevFinalOffset = Number.POSITIVE_INFINITY;
311
+ let finalOffset = 0;
312
+ let attempts = 0;
313
+ const MAX_ATTEMPTS = 5;
314
+ const OFFSET_TOLERANCE = 1; // 1px tolerance
315
+
316
+ do {
317
+ const layout = recyclerViewManager.getLayout(index);
318
+ if (!layout || isUnmounted.current) break;
319
+
320
+ const offset = horizontal ? layout.x : layout.y;
321
+ finalOffset = offset;
322
+
323
+ // Apply viewPosition and viewOffset adjustments if provided
324
+ if (viewPosition !== undefined || viewOffset !== undefined) {
325
+ const containerSize = horizontal
326
+ ? recyclerViewManager.getWindowSize().width
327
+ : recyclerViewManager.getWindowSize().height;
328
+
329
+ const itemSize = horizontal ? layout.width : layout.height;
330
+
331
+ if (viewPosition !== undefined) {
332
+ // viewPosition: 0 = top, 0.5 = center, 1 = bottom
333
+ finalOffset =
334
+ offset - (containerSize - itemSize) * viewPosition;
335
+ }
336
+
337
+ if (viewOffset !== undefined) {
338
+ finalOffset += viewOffset;
339
+ }
340
+ }
341
+
342
+ // Check if offset has stabilized
343
+ if (Math.abs(prevFinalOffset - finalOffset) <= OFFSET_TOLERANCE) {
344
+ break;
345
+ }
346
+
347
+ prevFinalOffset = finalOffset;
348
+
349
+ if (animated) {
350
+ if (finalOffset > lastScrollOffset) {
351
+ lastScrollOffset = Math.max(
352
+ finalOffset - bufferForCompute,
353
+ lastScrollOffset
354
+ );
355
+ } else {
356
+ lastScrollOffset = Math.min(
357
+ finalOffset + bufferForCompute,
358
+ lastScrollOffset
359
+ );
360
+ }
361
+
362
+ await updateScrollOffsetAsync(lastScrollOffset);
363
+ }
364
+ await updateScrollOffsetAsync(finalOffset);
365
+
366
+ attempts++;
367
+ } while (attempts < MAX_ATTEMPTS);
368
+
369
+ if (animated) {
370
+ const maxOffset =
371
+ (horizontal
372
+ ? recyclerViewManager.getChildContainerDimensions().width
373
+ : recyclerViewManager.getChildContainerDimensions().height) -
374
+ (horizontal
375
+ ? recyclerViewManager.getWindowSize().width
376
+ : recyclerViewManager.getWindowSize().height);
377
+
378
+ if (finalOffset > maxOffset) {
379
+ finalOffset = maxOffset;
380
+ }
381
+
382
+ if (finalOffset > lastScrollOffset) {
383
+ lastScrollOffset = Math.max(
384
+ finalOffset - bufferForScroll,
385
+ lastScrollOffset
386
+ );
387
+ } else {
388
+ lastScrollOffset = Math.min(
389
+ finalOffset + bufferForScroll,
390
+ lastScrollOffset
391
+ );
392
+ }
393
+
394
+ //We don't need to add firstItemOffset here as it will be added in scrollToOffset
395
+ handlerMethods.scrollToOffset({
396
+ offset: lastScrollOffset,
397
+ animated: false,
398
+ skipFirstItemOffset: false,
399
+ });
400
+ }
401
+
402
+ handlerMethods.scrollToOffset({
403
+ offset: finalOffset,
404
+ animated,
405
+ skipFirstItemOffset: false,
406
+ });
407
+ }
408
+ setTimeout(() => {
409
+ pauseAdjustRef.current = false;
410
+ }, 200);
411
+ }
412
+ },
413
+
414
+ /**
415
+ * Scrolls to a specific item in the list.
416
+ * Finds the item's index and uses scrollToIndex internally.
417
+ */
418
+ scrollToItem: ({
419
+ item,
420
+ animated,
421
+ viewPosition,
422
+ viewOffset,
423
+ }: ScrollToItemParams<T>) => {
424
+ if (scrollViewRef.current && data) {
425
+ // Find the index of the item in the data array
426
+ const index = Array.from(data).findIndex(
427
+ (dataItem) => dataItem === item
428
+ );
429
+ if (index >= 0) {
430
+ handlerMethods.scrollToIndex({
431
+ index,
432
+ animated,
433
+ viewPosition,
434
+ viewOffset,
435
+ });
436
+ }
437
+ }
438
+ },
439
+
440
+ // Utility methods for measuring header height / top padding
441
+ getFirstItemOffset: () => {
442
+ return recyclerViewManager.firstItemOffset;
443
+ },
444
+ getWindowSize: () => {
445
+ return recyclerViewManager.getWindowSize();
446
+ },
447
+ getLayout: (index: number) => {
448
+ return recyclerViewManager.getLayout(index);
449
+ },
450
+ getAbsoluteLastScrollOffset: () => {
451
+ return recyclerViewManager.getAbsoluteLastScrollOffset();
452
+ },
453
+ getChildContainerDimensions: () => {
454
+ return recyclerViewManager.getChildContainerDimensions();
455
+ },
456
+ recordInteraction: () => {
457
+ recyclerViewManager.recordInteraction();
458
+ },
459
+ getVisibleIndices: () => {
460
+ return recyclerViewManager.getVisibleIndices();
461
+ },
462
+ getFirstVisibleIndex: () => {
463
+ return recyclerViewManager.getVisibleIndices().startIndex;
464
+ },
465
+ recomputeViewableItems: () => {
466
+ recyclerViewManager.recomputeViewableItems();
467
+ },
468
+ /**
469
+ * Disables item recycling in preparation for layout animations.
470
+ */
471
+ prepareForLayoutAnimationRender: () => {
472
+ recyclerViewManager.disableRecycling = true;
473
+ },
474
+ };
475
+ }, [horizontal, data, recyclerViewManager]);
476
+
477
+ // Expose imperative methods through the ref
478
+ useImperativeHandle(
479
+ ref,
480
+ () => {
481
+ return { ...scrollViewRef.current, ...handlerMethods };
482
+ },
483
+ [handlerMethods]
484
+ );
485
+
486
+ return { applyContentOffset, applyInitialScrollIndex };
487
+ }
@@ -0,0 +1,30 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+
3
+ import { RecyclerViewProps } from "../RecyclerViewProps";
4
+ import { RecyclerViewManager } from "../RecyclerViewManager";
5
+
6
+ export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
7
+ const [recyclerViewManager] = useState<RecyclerViewManager<T>>(
8
+ () => new RecyclerViewManager(props)
9
+ );
10
+ const { data } = props;
11
+
12
+ useMemo(() => {
13
+ recyclerViewManager.updateProps(props);
14
+ }, [props]);
15
+
16
+ /**
17
+ * When data changes, we need to process the data update before the render happens
18
+ */
19
+ useMemo(() => {
20
+ recyclerViewManager.processDataUpdate();
21
+ }, [data]);
22
+
23
+ useEffect(() => {
24
+ return () => {
25
+ recyclerViewManager.dispose();
26
+ };
27
+ }, []);
28
+
29
+ return { recyclerViewManager };
30
+ };
@@ -0,0 +1,63 @@
1
+ import { Dispatch, SetStateAction, useCallback, useMemo, useRef } from "react";
2
+
3
+ import { useLayoutState } from "./useLayoutState";
4
+
5
+ /**
6
+ * A custom hook that provides state management with automatic reset functionality.
7
+ * Similar to useState, but automatically resets the state when specified dependencies change.
8
+ * This is particularly useful for managing state that needs to be reset when certain props or values change when items are recycled.
9
+ * This also avoids another setState call on recycling and helps with performance.
10
+ *
11
+ * @param initialState - The initial state value or a function that returns the initial state
12
+ * @param deps - Array of dependencies that trigger a state reset when changed
13
+ * @param onReset - Optional callback function that is called when the state is reset
14
+ * @returns A tuple containing:
15
+ * - The current state value
16
+ * - A setState function that works like useState's setState
17
+ */
18
+ export function useRecyclingState<T>(
19
+ initialState: T | (() => T),
20
+ deps: React.DependencyList,
21
+ onReset?: () => void
22
+ ): [T, Dispatch<SetStateAction<T>>] {
23
+ // Store the current state value in a ref to persist between renders
24
+ const valueStore = useRef<T>();
25
+ // Use layoutState to trigger re-renders when state changes
26
+ const [_, setCounter] = useLayoutState(0);
27
+
28
+ // Reset state when dependencies change
29
+ useMemo(() => {
30
+ // Calculate initial value from function or direct value
31
+ const initialValue =
32
+ typeof initialState === "function"
33
+ ? (initialState as () => T)()
34
+ : initialState;
35
+ valueStore.current = initialValue;
36
+ // Call onReset callback if provided
37
+ onReset?.();
38
+ // eslint-disable-next-line react-hooks/exhaustive-deps
39
+ }, deps);
40
+
41
+ /**
42
+ * Proxy setState function that updates the stored value and triggers a re-render.
43
+ * Only triggers a re-render if the new value is different from the current value.
44
+ */
45
+ const setStateProxy = useCallback(
46
+ (newValue: T | ((prevValue: T) => T)) => {
47
+ // Calculate next state value from function or direct value
48
+ const nextState =
49
+ typeof newValue === "function"
50
+ ? (newValue as (prevValue: T) => T)(valueStore.current!)
51
+ : newValue;
52
+
53
+ // Only update and trigger re-render if value has changed
54
+ if (nextState !== valueStore.current) {
55
+ valueStore.current = nextState;
56
+ setCounter((prev) => prev + 1);
57
+ }
58
+ },
59
+ [setCounter]
60
+ );
61
+
62
+ return [valueStore.current!, setStateProxy];
63
+ }
@@ -0,0 +1,119 @@
1
+ import { Animated, RefreshControl } from "react-native";
2
+ import { RecyclerViewProps } from "../RecyclerViewProps";
3
+ import { useMemo } from "react";
4
+ import { getValidComponent } from "../utils/componentUtils";
5
+ import { CompatView } from "../components/CompatView";
6
+ import { CompatAnimatedScroller } from "../components/CompatScroller";
7
+ import React from "react";
8
+
9
+ /**
10
+ * Hook that manages secondary props and components for the RecyclerView.
11
+ * This hook handles the creation and management of:
12
+ * 1. Pull-to-refresh functionality
13
+ * 2. Header and footer components
14
+ * 3. Empty state component
15
+ * 4. Custom scroll component with animation support
16
+ *
17
+ * @param props - The RecyclerViewProps containing all configuration options
18
+ * @returns An object containing:
19
+ * - refreshControl: The pull-to-refresh control component
20
+ * - renderHeader: The header component renderer
21
+ * - renderFooter: The footer component renderer
22
+ * - renderEmpty: The empty state component renderer
23
+ * - CompatScrollView: The animated scroll component
24
+ */
25
+ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
26
+ const {
27
+ ListHeaderComponent,
28
+ ListHeaderComponentStyle,
29
+ ListFooterComponent,
30
+ ListFooterComponentStyle,
31
+ ListEmptyComponent,
32
+ renderScrollComponent,
33
+ refreshing,
34
+ progressViewOffset,
35
+ onRefresh,
36
+ data,
37
+ } = props;
38
+
39
+ /**
40
+ * Creates the refresh control component if onRefresh is provided.
41
+ */
42
+ const refreshControl = useMemo(() => {
43
+ if (onRefresh) {
44
+ return (
45
+ <RefreshControl
46
+ refreshing={Boolean(refreshing)}
47
+ progressViewOffset={progressViewOffset}
48
+ onRefresh={onRefresh}
49
+ />
50
+ );
51
+ }
52
+ return undefined;
53
+ }, [onRefresh, refreshing, progressViewOffset]);
54
+
55
+ /**
56
+ * Creates the header component with optional styling.
57
+ */
58
+ const renderHeader = useMemo(() => {
59
+ if (!ListHeaderComponent) {
60
+ return null;
61
+ }
62
+ return (
63
+ <CompatView style={ListHeaderComponentStyle}>
64
+ {getValidComponent(ListHeaderComponent)}
65
+ </CompatView>
66
+ );
67
+ }, [ListHeaderComponent, ListHeaderComponentStyle]);
68
+
69
+ /**
70
+ * Creates the footer component with optional styling.
71
+ */
72
+ const renderFooter = useMemo(() => {
73
+ if (!ListFooterComponent) {
74
+ return null;
75
+ }
76
+ return (
77
+ <CompatView style={ListFooterComponentStyle}>
78
+ {getValidComponent(ListFooterComponent)}
79
+ </CompatView>
80
+ );
81
+ }, [ListFooterComponent, ListFooterComponentStyle]);
82
+
83
+ /**
84
+ * Creates the empty state component when there's no data.
85
+ * Only rendered when ListEmptyComponent is provided and data is empty.
86
+ */
87
+ const renderEmpty = useMemo(() => {
88
+ if (!ListEmptyComponent || (data && data.length > 0)) {
89
+ return null;
90
+ }
91
+ return getValidComponent(ListEmptyComponent);
92
+ }, [ListEmptyComponent, data]);
93
+
94
+ /**
95
+ * Creates an animated scroll component based on the provided renderScrollComponent.
96
+ * If no custom component is provided, uses the default CompatAnimatedScroller.
97
+ */
98
+ const CompatScrollView = useMemo(() => {
99
+ let scrollComponent = CompatAnimatedScroller;
100
+ if (typeof renderScrollComponent === "function") {
101
+ // Create a forwarded ref wrapper for the custom scroll component
102
+ scrollComponent = React.forwardRef((props, ref) =>
103
+ (renderScrollComponent as any)({ ...props, ref } as any)
104
+ ) as any;
105
+ } else if (renderScrollComponent) {
106
+ scrollComponent = renderScrollComponent;
107
+ }
108
+ // Wrap the scroll component with Animated.createAnimatedComponent
109
+ return Animated.createAnimatedComponent(scrollComponent);
110
+ }, [renderScrollComponent]);
111
+
112
+ return {
113
+ refreshControl,
114
+ renderHeader,
115
+ renderFooter,
116
+ renderEmpty,
117
+ CompatScrollView,
118
+ };
119
+ }