@shopify/flash-list 1.8.0 → 2.0.0-alpha.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 (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 +8 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +19 -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 +14 -1
  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,191 @@
1
+ import { ConsecutiveNumbers } from "./ConsecutiveNumbers";
2
+ import { RVLayoutManager } from "../layout-managers/LayoutManager";
3
+
4
+ export interface RVEngagedIndicesTracker {
5
+ // Current scroll offset of the list. Directly setting this won't trigger visible indices updates
6
+ scrollOffset: number;
7
+ // Total distance (in pixels) to pre-render items before and after the visible viewport
8
+ drawDistance: number;
9
+
10
+ /**
11
+ * Updates the scroll offset and calculates which items should be rendered (engaged indices).
12
+ * @param offset - The new scroll offset position
13
+ * @param velocity - Current scroll velocity to optimize buffer distribution
14
+ * @param layoutManager - Layout manager to fetch item positions and dimensions
15
+ * @returns New engaged indices if changed, undefined if no change
16
+ */
17
+ updateScrollOffset: (
18
+ offset: number,
19
+ velocity: Velocity | null | undefined,
20
+ layoutManager: RVLayoutManager
21
+ ) => ConsecutiveNumbers | undefined;
22
+ getEngagedIndices: () => ConsecutiveNumbers;
23
+ computeVisibleIndices: (layoutManager: RVLayoutManager) => ConsecutiveNumbers;
24
+ }
25
+
26
+ export interface Velocity {
27
+ x: number;
28
+ y: number;
29
+ }
30
+
31
+ export class RVEngagedIndicesTrackerImpl implements RVEngagedIndicesTracker {
32
+ // Current scroll position of the list
33
+ public scrollOffset = 0;
34
+ // Distance to pre-render items before and after the visible viewport (in pixels)
35
+ public drawDistance: number = 250;
36
+ // Currently rendered item indices (including buffer items)
37
+ private engagedIndices = ConsecutiveNumbers.EMPTY;
38
+
39
+ // Buffer distribution multipliers for scroll direction optimization
40
+ private smallMultiplier = 0.1; // Used for buffer in the opposite direction of scroll
41
+ private largeMultiplier = 0.9; // Used for buffer in the direction of scroll
42
+
43
+ // Circular buffer to track recent scroll velocities for direction detection
44
+ private velocityHistory = [-1, -1, -1, -1, -1];
45
+ private velocityIndex = 0;
46
+
47
+ /**
48
+ * Updates scroll position and determines which items should be rendered.
49
+ * Implements a smart buffer system that:
50
+ * 1. Calculates the visible viewport
51
+ * 2. Determines optimal buffer distribution based on scroll direction
52
+ * 3. Adjusts buffer sizes at list boundaries
53
+ * 4. Returns new indices that need to be rendered
54
+ */
55
+ updateScrollOffset(
56
+ offset: number,
57
+ velocity: Velocity | null | undefined,
58
+ layoutManager: RVLayoutManager
59
+ ): ConsecutiveNumbers | undefined {
60
+ // Update current scroll position
61
+ this.scrollOffset = offset;
62
+
63
+ // STEP 1: Determine the currently visible viewport
64
+ const windowSize = layoutManager.getWindowsSize();
65
+ const isHorizontal = layoutManager.isHorizontal();
66
+ const viewportStart = offset;
67
+ const viewportSize = isHorizontal ? windowSize.width : windowSize.height;
68
+ const viewportEnd = viewportStart + viewportSize;
69
+
70
+ // STEP 2: Determine buffer size and distribution
71
+ // The total extra space where items will be pre-rendered
72
+ const totalBuffer = this.drawDistance * 2;
73
+
74
+ // Determine scroll direction to optimize buffer distribution
75
+ const isScrollingBackward = this.isScrollingBackward(
76
+ isHorizontal ? velocity?.x : velocity?.y
77
+ );
78
+
79
+ // Distribute more buffer in the direction of scrolling
80
+ // When scrolling forward: more buffer after viewport
81
+ // When scrolling backward: more buffer before viewport
82
+ const beforeRatio = isScrollingBackward
83
+ ? this.largeMultiplier
84
+ : this.smallMultiplier;
85
+ const afterRatio = isScrollingBackward
86
+ ? this.smallMultiplier
87
+ : this.largeMultiplier;
88
+
89
+ let bufferBefore = Math.ceil(totalBuffer * beforeRatio);
90
+ let bufferAfter = Math.ceil(totalBuffer * afterRatio);
91
+
92
+ // STEP 3: Calculate the extended viewport (visible area + buffers)
93
+ // The start position with buffer (never less than 0)
94
+ let extendedStart = Math.max(0, viewportStart - bufferBefore);
95
+
96
+ // If we couldn't apply full buffer at start, calculate how much was unused
97
+ const unusedStartBuffer = Math.max(0, bufferBefore - viewportStart);
98
+
99
+ // Add any unused start buffer to the end buffer
100
+ let extendedEnd = viewportEnd + bufferAfter + unusedStartBuffer;
101
+
102
+ // STEP 4: Handle end boundary adjustments
103
+ // Get the total content size to check for end boundary
104
+ const layoutSize = layoutManager.getLayoutSize();
105
+ const maxPosition = isHorizontal ? layoutSize.width : layoutSize.height;
106
+
107
+ // If we hit the end boundary, redistribute unused buffer to the start
108
+ if (extendedEnd > maxPosition) {
109
+ // Calculate unused end buffer and apply it to the start if possible
110
+ const unusedEndBuffer = extendedEnd - maxPosition;
111
+ extendedEnd = maxPosition;
112
+
113
+ // Try to extend start position further with the unused end buffer
114
+ extendedStart = Math.max(0, extendedStart - unusedEndBuffer);
115
+ }
116
+
117
+ // STEP 5: Get and return the new engaged indices
118
+ const newEngagedIndices = layoutManager.getVisibleLayouts(
119
+ extendedStart,
120
+ extendedEnd
121
+ );
122
+ if (!isHorizontal) {
123
+ //console.log("newEngagedIndices", newEngagedIndices, this.scrollOffset);
124
+ }
125
+ // Only return new indices if they've changed
126
+ const oldEngagedIndices = this.engagedIndices;
127
+ this.engagedIndices = newEngagedIndices;
128
+
129
+ return newEngagedIndices.equals(oldEngagedIndices)
130
+ ? undefined
131
+ : newEngagedIndices;
132
+ }
133
+
134
+ /**
135
+ * Determines scroll direction by analyzing recent velocity history.
136
+ * Uses a majority voting system on the last 5 velocity values.
137
+ * @param velocity - Current scroll velocity component (x or y)
138
+ * @returns true if scrolling backward (negative direction), false otherwise
139
+ */
140
+ private isScrollingBackward(velocity?: number): boolean {
141
+ //update velocity history
142
+ if (velocity) {
143
+ this.velocityHistory[this.velocityIndex] = velocity;
144
+ this.velocityIndex =
145
+ (this.velocityIndex + 1) % this.velocityHistory.length;
146
+ }
147
+ //should decide based on whether we have more positive or negative values, use for loop
148
+ let positiveCount = 0;
149
+ let negativeCount = 0;
150
+ for (let i = 0; i < this.velocityHistory.length; i++) {
151
+ if (this.velocityHistory[i] > 0) {
152
+ positiveCount++;
153
+ } else if (this.velocityHistory[i] < 0) {
154
+ negativeCount++;
155
+ }
156
+ }
157
+ return positiveCount < negativeCount;
158
+ }
159
+
160
+ /**
161
+ * Calculates which items are currently visible in the viewport.
162
+ * Unlike getEngagedIndices, this doesn't include buffer items.
163
+ * @param layoutManager - Layout manager to fetch item positions
164
+ * @returns Indices of items currently visible in the viewport
165
+ */
166
+ computeVisibleIndices(layoutManager: RVLayoutManager): ConsecutiveNumbers {
167
+ const windowSize = layoutManager.getWindowsSize();
168
+ const isHorizontal = layoutManager.isHorizontal();
169
+
170
+ // Calculate viewport boundaries
171
+ const viewportStart = this.scrollOffset;
172
+ const viewportSize = isHorizontal ? windowSize.width : windowSize.height;
173
+ const viewportEnd = viewportStart + viewportSize;
174
+
175
+ // Get indices of items currently visible in the viewport
176
+ const newVisibleIndices = layoutManager.getVisibleLayouts(
177
+ viewportStart,
178
+ viewportEnd
179
+ );
180
+ return newVisibleIndices;
181
+ }
182
+
183
+ /**
184
+ * Returns the currently engaged (rendered) indices.
185
+ * This includes both visible items and buffer items.
186
+ * @returns The last computed set of engaged indices
187
+ */
188
+ getEngagedIndices(): ConsecutiveNumbers {
189
+ return this.engagedIndices;
190
+ }
191
+ }
@@ -0,0 +1,127 @@
1
+ import { useCallback, useEffect, useMemo } from "react";
2
+ import { useRef } from "react";
3
+ import { RecyclerViewManager } from "../RecyclerViewManager";
4
+ import { RecyclerViewProps } from "../RecyclerViewProps";
5
+ import { CompatScroller } from "../components/CompatScroller";
6
+
7
+ /**
8
+ * Hook to detect when the scroll position reaches near the start or end of the list
9
+ * and trigger the appropriate callbacks. This hook is responsible for:
10
+ * 1. Detecting when the user scrolls near the end of the list (onEndReached)
11
+ * 2. Detecting when the user scrolls near the start of the list (onStartReached)
12
+ * 3. Managing auto-scrolling to bottom when new content is added
13
+ *
14
+ * @param recyclerViewManager - The RecyclerViewManager instance that handles the list's core functionality
15
+ * @param props - The RecyclerViewProps containing configuration and callbacks
16
+ * @param scrollViewRef - Reference to the scrollable container component
17
+ */
18
+ export function useBoundDetection<T>(
19
+ recyclerViewManager: RecyclerViewManager<T>,
20
+ props: RecyclerViewProps<T>,
21
+ scrollViewRef: React.RefObject<CompatScroller>
22
+ ) {
23
+ // Track whether we've already triggered the end reached callback to prevent duplicate calls
24
+ const pendingEndReached = useRef(false);
25
+ // Track whether we've already triggered the start reached callback to prevent duplicate calls
26
+ const pendingStartReached = useRef(false);
27
+ // Track whether we should auto-scroll to bottom when new content is added
28
+ const pendingAutoscrollToBottom = useRef(false);
29
+ const { horizontal, data, maintainVisibleContentPosition } = props;
30
+
31
+ /**
32
+ * Checks if the scroll position is near the start or end of the list
33
+ * and triggers appropriate callbacks if configured.
34
+ */
35
+ const checkBounds = useCallback(() => {
36
+ // Skip all calculations if neither callback is provided and autoscroll is disabled
37
+ const autoscrollToBottomThreshold =
38
+ maintainVisibleContentPosition?.autoscrollToBottomThreshold ?? -1;
39
+
40
+ if (
41
+ !props.onEndReached &&
42
+ !props.onStartReached &&
43
+ autoscrollToBottomThreshold < 0
44
+ ) {
45
+ return;
46
+ }
47
+
48
+ if (recyclerViewManager.getIsFirstLayoutComplete()) {
49
+ const lastScrollOffset =
50
+ recyclerViewManager.getAbsoluteLastScrollOffset();
51
+ const contentSize = recyclerViewManager.getChildContainerDimensions();
52
+ const windowSize = recyclerViewManager.getWindowSize();
53
+ const isHorizontal = props.horizontal === true;
54
+
55
+ // Calculate dimensions based on scroll direction
56
+ const visibleLength = isHorizontal ? windowSize.width : windowSize.height;
57
+ const contentLength =
58
+ (isHorizontal ? contentSize.width : contentSize.height) +
59
+ recyclerViewManager.firstItemOffset;
60
+
61
+ // Check if we're near the end of the list
62
+ if (props.onEndReached) {
63
+ const onEndReachedThreshold = props.onEndReachedThreshold ?? 0.5;
64
+ const endThresholdDistance = onEndReachedThreshold * visibleLength;
65
+
66
+ const isNearEnd =
67
+ Math.ceil(lastScrollOffset + visibleLength) >=
68
+ contentLength - endThresholdDistance;
69
+
70
+ if (isNearEnd && !pendingEndReached.current) {
71
+ pendingEndReached.current = true;
72
+ props.onEndReached();
73
+ }
74
+ pendingEndReached.current = isNearEnd;
75
+ }
76
+
77
+ // Check if we're near the start of the list
78
+ if (props.onStartReached) {
79
+ const onStartReachedThreshold = props.onStartReachedThreshold ?? 0.2;
80
+ const startThresholdDistance = onStartReachedThreshold * visibleLength;
81
+
82
+ const isNearStart = lastScrollOffset <= startThresholdDistance;
83
+
84
+ if (isNearStart && !pendingStartReached.current) {
85
+ pendingStartReached.current = true;
86
+ props.onStartReached();
87
+ }
88
+ pendingStartReached.current = isNearStart;
89
+ }
90
+
91
+ // Handle auto-scrolling to bottom for vertical lists
92
+ if (!horizontal) {
93
+ const autoscrollToBottomThresholdDistance =
94
+ autoscrollToBottomThreshold * visibleLength;
95
+
96
+ const isNearBottom =
97
+ Math.ceil(lastScrollOffset + visibleLength) >=
98
+ contentLength - autoscrollToBottomThresholdDistance;
99
+
100
+ if (isNearBottom) {
101
+ pendingAutoscrollToBottom.current = true;
102
+ } else {
103
+ pendingAutoscrollToBottom.current = false;
104
+ }
105
+ }
106
+ }
107
+ }, [recyclerViewManager, props]);
108
+
109
+ // Reset end reached state when data changes
110
+ useMemo(() => {
111
+ pendingEndReached.current = false;
112
+ }, [data]);
113
+
114
+ // Auto-scroll to bottom when new content is added and we're near the bottom
115
+ useEffect(() => {
116
+ if (pendingAutoscrollToBottom.current) {
117
+ requestAnimationFrame(() => {
118
+ scrollViewRef.current?.scrollToEnd();
119
+ pendingAutoscrollToBottom.current = false;
120
+ });
121
+ }
122
+ }, [data]);
123
+
124
+ return {
125
+ checkBounds,
126
+ };
127
+ }
@@ -0,0 +1,46 @@
1
+ import { useState, useCallback } from "react";
2
+
3
+ import { useRecyclerViewContext } from "../RecyclerViewContextProvider";
4
+
5
+ /**
6
+ * Custom hook that combines state management with RecyclerView layout updates.
7
+ * This hook provides a way to manage state that affects the layout of the RecyclerView,
8
+ * ensuring that any state changes trigger a layout recalculation.
9
+ *
10
+ * @param initialState - The initial state value or a function that returns the initial state
11
+ * @returns A tuple containing:
12
+ * - The current state value
13
+ * - A setter function that updates the state and triggers a layout recalculation
14
+ */
15
+ export function useLayoutState<T>(
16
+ initialState: T | (() => T)
17
+ ): [T, (newValue: T | ((prevValue: T) => T)) => void] {
18
+ // Initialize state with the provided initial value
19
+ const [state, setState] = useState<T>(initialState);
20
+ // Get the RecyclerView context for layout management
21
+ const recyclerViewContext = useRecyclerViewContext();
22
+
23
+ /**
24
+ * Setter function that updates the state and triggers a layout recalculation.
25
+ * This ensures that any state changes that affect the layout are properly reflected
26
+ * in the RecyclerView's visual representation.
27
+ *
28
+ * @param newValue - Either a new state value or a function that receives the previous state
29
+ * and returns the new state
30
+ */
31
+ const setLayoutState = useCallback(
32
+ (newValue: T | ((prevValue: T) => T)) => {
33
+ // Update the state using either the new value or the result of the updater function
34
+ setState((prevValue) =>
35
+ typeof newValue === "function"
36
+ ? (newValue as (prevValue: T) => T)(prevValue)
37
+ : newValue
38
+ );
39
+ // Trigger a layout recalculation in the RecyclerView
40
+ recyclerViewContext?.layout();
41
+ },
42
+ [recyclerViewContext]
43
+ );
44
+
45
+ return [state, setLayoutState];
46
+ }
@@ -0,0 +1,78 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { RecyclerViewManager } from "../RecyclerViewManager";
3
+ import { useUnmountFlag } from "./useUnmountFlag";
4
+ //import { ToastAndroid } from "react-native";
5
+
6
+ /**
7
+ * Hook to track when the RecyclerView has loaded its items and notify when loading is complete.
8
+ * Similar to FlashList's onLoad functionality, this hook tracks the time it takes to render
9
+ * the initial set of items in the RecyclerView and provides performance metrics.
10
+ *
11
+ * @param recyclerViewManager - The RecyclerViewManager instance managing the list
12
+ * @param onLoad - Optional callback function that will be called when the list has loaded with timing information
13
+ * @returns Object containing isLoaded state indicating whether the list has completed initial rendering
14
+ */
15
+ export const useOnListLoad = <T>(
16
+ recyclerViewManager: RecyclerViewManager<T>,
17
+ onLoad?: (info: { elapsedTimeInMs: number }) => void
18
+ ): { isLoaded: boolean } => {
19
+ const loadStartTimeRef = useRef<number>(Date.now());
20
+ const [isLoaded, setIsLoaded] = useState<boolean>(false);
21
+ const dataLength = recyclerViewManager.getDataLength();
22
+ //const dataCollector = useRef<number[]>([]);
23
+ const isUnmounted = useUnmountFlag();
24
+ // Track render cycles by collecting elapsed time on each render
25
+ // useEffect(() => {
26
+ // const elapsedTimeInMs = Date.now() - loadStartTimeRef.current;
27
+ // dataCollector.current?.push(elapsedTimeInMs);
28
+ // });
29
+
30
+ useMemo(() => {
31
+ loadStartTimeRef.current = Date.now();
32
+ }, [dataLength]);
33
+
34
+ useOnLoad(recyclerViewManager, () => {
35
+ const elapsedTimeInMs = Date.now() - loadStartTimeRef.current;
36
+ // Commented code below was used for debugging purposes
37
+ // to display all collected timing data points
38
+ // const dataCollectorString = dataCollector.current
39
+ // ?.map((value) => value.toString())
40
+ // .join(", ");
41
+ // ToastAndroid?.show(
42
+ // `onLoad called after ${dataCollectorString}`,
43
+ // ToastAndroid.SHORT
44
+ // );
45
+ //console.log("----------> dataCollector", dataCollectorString);
46
+ //console.log("----------> elapsedTimeInMs", elapsedTimeInMs);
47
+ requestAnimationFrame(() => {
48
+ if (!isUnmounted.current) {
49
+ onLoad?.({ elapsedTimeInMs });
50
+ setIsLoaded(true);
51
+ }
52
+ });
53
+ });
54
+
55
+ return { isLoaded };
56
+ };
57
+
58
+ /**
59
+ * Core hook that detects when a RecyclerView has completed its initial layout.
60
+ * This hook monitors the RecyclerViewManager and triggers the provided callback
61
+ * once the first layout is complete.
62
+ *
63
+ * @param recyclerViewManager - The RecyclerViewManager instance to monitor
64
+ * @param onLoad - Callback function that will be called once when the first layout is complete
65
+ */
66
+ export const useOnLoad = <T>(
67
+ recyclerViewManager: RecyclerViewManager<T>,
68
+ onLoad: () => void
69
+ ) => {
70
+ const isLoaded = useRef<boolean>(false);
71
+ useEffect(() => {
72
+ // Only trigger onLoad callback once when first layout is complete
73
+ if (recyclerViewManager.getIsFirstLayoutComplete() && !isLoaded.current) {
74
+ isLoaded.current = true;
75
+ onLoad();
76
+ }
77
+ });
78
+ };