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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +37 -97
  2. package/android/src/main/kotlin/com/shopify/reactnative/flash_list/BlankAreaEvent.kt +2 -2
  3. package/dist/AnimatedFlashList.d.ts.map +1 -1
  4. package/dist/AnimatedFlashList.js +3 -3
  5. package/dist/AnimatedFlashList.js.map +1 -1
  6. package/dist/FlashList.d.ts +9 -0
  7. package/dist/FlashList.d.ts.map +1 -1
  8. package/dist/FlashList.js +20 -0
  9. package/dist/FlashList.js.map +1 -1
  10. package/dist/FlashListProps.d.ts +30 -10
  11. package/dist/FlashListProps.d.ts.map +1 -1
  12. package/dist/FlashListProps.js.map +1 -1
  13. package/dist/FlashListRef.d.ts +305 -0
  14. package/dist/FlashListRef.d.ts.map +1 -0
  15. package/dist/FlashListRef.js +3 -0
  16. package/dist/FlashListRef.js.map +1 -0
  17. package/dist/MasonryFlashList.js.map +1 -1
  18. package/dist/__tests__/RecyclerView.test.js +72 -28
  19. package/dist/__tests__/RecyclerView.test.js.map +1 -1
  20. package/dist/__tests__/RenderStackManager.test.d.ts +2 -0
  21. package/dist/__tests__/RenderStackManager.test.d.ts.map +1 -0
  22. package/dist/__tests__/RenderStackManager.test.js +485 -0
  23. package/dist/__tests__/RenderStackManager.test.js.map +1 -0
  24. package/dist/__tests__/helpers/createLayoutManager.d.ts.map +1 -1
  25. package/dist/__tests__/helpers/createLayoutManager.js +3 -4
  26. package/dist/__tests__/helpers/createLayoutManager.js.map +1 -1
  27. package/dist/__tests__/useUnmountAwareCallbacks.test.js +1 -1
  28. package/dist/__tests__/useUnmountAwareCallbacks.test.js.map +1 -1
  29. package/dist/benchmark/useBenchmark.js +0 -25
  30. package/dist/benchmark/useBenchmark.js.map +1 -1
  31. package/dist/benchmark/useFlatListBenchmark.js +8 -7
  32. package/dist/benchmark/useFlatListBenchmark.js.map +1 -1
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/native/config/PlatformHelper.android.d.ts +1 -0
  38. package/dist/native/config/PlatformHelper.android.d.ts.map +1 -1
  39. package/dist/native/config/PlatformHelper.android.js +1 -0
  40. package/dist/native/config/PlatformHelper.android.js.map +1 -1
  41. package/dist/native/config/PlatformHelper.d.ts +1 -0
  42. package/dist/native/config/PlatformHelper.d.ts.map +1 -1
  43. package/dist/native/config/PlatformHelper.ios.d.ts +1 -0
  44. package/dist/native/config/PlatformHelper.ios.d.ts.map +1 -1
  45. package/dist/native/config/PlatformHelper.ios.js +1 -0
  46. package/dist/native/config/PlatformHelper.ios.js.map +1 -1
  47. package/dist/native/config/PlatformHelper.js +1 -0
  48. package/dist/native/config/PlatformHelper.js.map +1 -1
  49. package/dist/native/config/PlatformHelper.web.d.ts +1 -0
  50. package/dist/native/config/PlatformHelper.web.d.ts.map +1 -1
  51. package/dist/native/config/PlatformHelper.web.js +1 -0
  52. package/dist/native/config/PlatformHelper.web.js.map +1 -1
  53. package/dist/recyclerview/RecyclerView.d.ts +2 -1
  54. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  55. package/dist/recyclerview/RecyclerView.js +104 -57
  56. package/dist/recyclerview/RecyclerView.js.map +1 -1
  57. package/dist/recyclerview/RecyclerViewContextProvider.d.ts +41 -6
  58. package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -1
  59. package/dist/recyclerview/RecyclerViewContextProvider.js +4 -0
  60. package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -1
  61. package/dist/recyclerview/RecyclerViewManager.d.ts +24 -7
  62. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
  63. package/dist/recyclerview/RecyclerViewManager.js +119 -113
  64. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  65. package/dist/recyclerview/RenderStackManager.d.ts +86 -0
  66. package/dist/recyclerview/RenderStackManager.d.ts.map +1 -0
  67. package/dist/recyclerview/RenderStackManager.js +343 -0
  68. package/dist/recyclerview/RenderStackManager.js.map +1 -0
  69. package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
  70. package/dist/recyclerview/ViewHolder.js +5 -3
  71. package/dist/recyclerview/ViewHolder.js.map +1 -1
  72. package/dist/recyclerview/ViewHolderCollection.d.ts +9 -3
  73. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
  74. package/dist/recyclerview/ViewHolderCollection.js +26 -9
  75. package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
  76. package/dist/recyclerview/components/ScrollAnchor.d.ts +2 -2
  77. package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -1
  78. package/dist/recyclerview/components/ScrollAnchor.js +9 -5
  79. package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
  80. package/dist/recyclerview/components/StickyHeaders.d.ts +1 -1
  81. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
  82. package/dist/recyclerview/components/StickyHeaders.js +40 -33
  83. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  84. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts +45 -1
  85. package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts.map +1 -1
  86. package/dist/recyclerview/helpers/EngagedIndicesTracker.js +77 -20
  87. package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -1
  88. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts +11 -0
  89. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts.map +1 -0
  90. package/dist/recyclerview/helpers/RenderTimeTracker.js +42 -0
  91. package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -0
  92. package/dist/recyclerview/helpers/VelocityTracker.d.ts +29 -0
  93. package/dist/recyclerview/helpers/VelocityTracker.d.ts.map +1 -0
  94. package/dist/recyclerview/helpers/VelocityTracker.js +70 -0
  95. package/dist/recyclerview/helpers/VelocityTracker.js.map +1 -0
  96. package/dist/recyclerview/hooks/useBoundDetection.d.ts +1 -2
  97. package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -1
  98. package/dist/recyclerview/hooks/useBoundDetection.js +56 -22
  99. package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
  100. package/dist/recyclerview/hooks/useLayoutState.d.ts +3 -1
  101. package/dist/recyclerview/hooks/useLayoutState.d.ts.map +1 -1
  102. package/dist/recyclerview/hooks/useLayoutState.js +5 -3
  103. package/dist/recyclerview/hooks/useLayoutState.js.map +1 -1
  104. package/dist/recyclerview/hooks/useMappingHelper.d.ts +1 -1
  105. package/dist/recyclerview/hooks/useMappingHelper.d.ts.map +1 -1
  106. package/dist/recyclerview/hooks/useMappingHelper.js +1 -1
  107. package/dist/recyclerview/hooks/useMappingHelper.js.map +1 -1
  108. package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -1
  109. package/dist/recyclerview/hooks/useOnLoad.js +4 -6
  110. package/dist/recyclerview/hooks/useOnLoad.js.map +1 -1
  111. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +5 -49
  112. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  113. package/dist/recyclerview/hooks/useRecyclerViewController.js +315 -204
  114. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  115. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts +2 -0
  116. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
  117. package/dist/recyclerview/hooks/useRecyclerViewManager.js +11 -1
  118. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
  119. package/dist/recyclerview/hooks/useRecyclingState.d.ts +4 -2
  120. package/dist/recyclerview/hooks/useRecyclingState.d.ts.map +1 -1
  121. package/dist/recyclerview/hooks/useRecyclingState.js +2 -2
  122. package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -1
  123. package/dist/recyclerview/hooks/useSecondaryProps.js +1 -1
  124. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts +10 -3
  125. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts.map +1 -1
  126. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js +33 -4
  127. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js.map +1 -1
  128. package/dist/recyclerview/hooks/useUnmountFlag.d.ts.map +1 -1
  129. package/dist/recyclerview/hooks/useUnmountFlag.js +1 -0
  130. package/dist/recyclerview/hooks/useUnmountFlag.js.map +1 -1
  131. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +18 -4
  132. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  133. package/dist/recyclerview/layout-managers/GridLayoutManager.js +60 -21
  134. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  135. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +35 -21
  136. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
  137. package/dist/recyclerview/layout-managers/LayoutManager.js +92 -28
  138. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
  139. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts +9 -1
  140. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -1
  141. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +28 -12
  142. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
  143. package/dist/recyclerview/utils/measureLayout.web.d.ts.map +1 -1
  144. package/dist/recyclerview/utils/measureLayout.web.js +1 -3
  145. package/dist/recyclerview/utils/measureLayout.web.js.map +1 -1
  146. package/dist/tsconfig.tsbuildinfo +1 -1
  147. package/dist/viewability/ViewToken.d.ts +2 -2
  148. package/dist/viewability/ViewToken.d.ts.map +1 -1
  149. package/dist/viewability/ViewabilityHelper.js +1 -1
  150. package/dist/viewability/ViewabilityHelper.js.map +1 -1
  151. package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
  152. package/dist/viewability/ViewabilityManager.js +11 -5
  153. package/dist/viewability/ViewabilityManager.js.map +1 -1
  154. package/jestSetup.js +30 -11
  155. package/package.json +2 -1
  156. package/src/AnimatedFlashList.ts +3 -2
  157. package/src/FlashList.tsx +24 -0
  158. package/src/FlashListProps.ts +41 -10
  159. package/src/FlashListRef.ts +320 -0
  160. package/src/MasonryFlashList.tsx +2 -2
  161. package/src/__tests__/RecyclerView.test.tsx +106 -31
  162. package/src/__tests__/RenderStackManager.test.ts +574 -0
  163. package/src/__tests__/helpers/createLayoutManager.ts +2 -3
  164. package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
  165. package/src/benchmark/useBenchmark.ts +0 -37
  166. package/src/benchmark/useFlatListBenchmark.ts +2 -2
  167. package/src/index.ts +2 -1
  168. package/src/native/config/PlatformHelper.android.ts +1 -0
  169. package/src/native/config/PlatformHelper.ios.ts +1 -0
  170. package/src/native/config/PlatformHelper.ts +1 -0
  171. package/src/native/config/PlatformHelper.web.ts +1 -0
  172. package/src/recyclerview/RecyclerView.tsx +139 -75
  173. package/src/recyclerview/RecyclerViewContextProvider.ts +52 -7
  174. package/src/recyclerview/RecyclerViewManager.ts +135 -98
  175. package/src/recyclerview/RenderStackManager.ts +317 -0
  176. package/src/recyclerview/ViewHolder.tsx +5 -3
  177. package/src/recyclerview/ViewHolderCollection.tsx +42 -14
  178. package/src/recyclerview/components/ScrollAnchor.tsx +21 -9
  179. package/src/recyclerview/components/StickyHeaders.tsx +63 -45
  180. package/src/recyclerview/helpers/EngagedIndicesTracker.ts +118 -23
  181. package/src/recyclerview/helpers/RenderTimeTracker.ts +42 -0
  182. package/src/recyclerview/helpers/VelocityTracker.ts +77 -0
  183. package/src/recyclerview/hooks/useBoundDetection.ts +72 -23
  184. package/src/recyclerview/hooks/useLayoutState.ts +15 -6
  185. package/src/recyclerview/hooks/useMappingHelper.ts +1 -1
  186. package/src/recyclerview/hooks/useOnLoad.ts +4 -6
  187. package/src/recyclerview/hooks/useRecyclerViewController.tsx +364 -254
  188. package/src/recyclerview/hooks/useRecyclerViewManager.ts +13 -1
  189. package/src/recyclerview/hooks/useRecyclingState.ts +11 -7
  190. package/src/recyclerview/hooks/useSecondaryProps.tsx +1 -1
  191. package/src/recyclerview/hooks/useUnmountAwareCallbacks.ts +39 -3
  192. package/src/recyclerview/hooks/useUnmountFlag.ts +1 -0
  193. package/src/recyclerview/layout-managers/GridLayoutManager.ts +67 -23
  194. package/src/recyclerview/layout-managers/LayoutManager.ts +110 -41
  195. package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +30 -8
  196. package/src/recyclerview/utils/measureLayout.web.ts +1 -3
  197. package/src/viewability/ViewToken.ts +2 -2
  198. package/src/viewability/ViewabilityHelper.ts +1 -1
  199. package/src/viewability/ViewabilityManager.ts +16 -9
  200. package/dist/__tests__/RecycleKeyManager.test.d.ts +0 -2
  201. package/dist/__tests__/RecycleKeyManager.test.d.ts.map +0 -1
  202. package/dist/__tests__/RecycleKeyManager.test.js +0 -210
  203. package/dist/__tests__/RecycleKeyManager.test.js.map +0 -1
  204. package/dist/recyclerview/RecycleKeyManager.d.ts +0 -82
  205. package/dist/recyclerview/RecycleKeyManager.d.ts.map +0 -1
  206. package/dist/recyclerview/RecycleKeyManager.js +0 -135
  207. package/dist/recyclerview/RecycleKeyManager.js.map +0 -1
  208. package/src/__tests__/RecycleKeyManager.test.ts +0 -254
  209. package/src/recyclerview/RecycleKeyManager.ts +0 -185
@@ -0,0 +1,317 @@
1
+ import { ConsecutiveNumbers } from "./helpers/ConsecutiveNumbers";
2
+
3
+ /**
4
+ * Manages the recycling of rendered items in a virtualized list.
5
+ * This class handles tracking, recycling, and reusing item keys to optimize
6
+ * rendering performance by minimizing creation/destruction of components.
7
+ */
8
+ export class RenderStackManager {
9
+ public disableRecycling = false;
10
+
11
+ // Maximum number of items that can be in the recycle pool
12
+ private maxItemsInRecyclePool: number;
13
+
14
+ // Stores pools of recycled keys for each item type
15
+ private recycleKeyPools: Map<string, Set<string>>;
16
+
17
+ // Maps active keys to their metadata (item type and stable ID)
18
+ private keyMap: Map<
19
+ string,
20
+ { itemType: string; index: number; stableId: string }
21
+ >;
22
+
23
+ // Maps stable IDs to their corresponding keys for quick lookups
24
+ private stableIdMap: Map<string, string>;
25
+
26
+ // Counter for generating unique sequential keys
27
+ private keyCounter: number;
28
+
29
+ private unProcessedIndices: Set<number>;
30
+
31
+ /**
32
+ * @param maxItemsInRecyclePool - Maximum number of items that can be in the recycle pool
33
+ */
34
+ constructor(maxItemsInRecyclePool: number = Number.MAX_SAFE_INTEGER) {
35
+ this.maxItemsInRecyclePool = maxItemsInRecyclePool;
36
+ this.recycleKeyPools = new Map();
37
+ this.keyMap = new Map();
38
+ this.stableIdMap = new Map();
39
+ this.keyCounter = 0;
40
+ this.unProcessedIndices = new Set();
41
+ }
42
+
43
+ /**
44
+ * Synchronizes the render stack with the current state of data.
45
+ * This method is the core orchestrator that:
46
+ * 1. Recycles keys for items that are no longer valid
47
+ * 2. Updates existing keys for items that remain visible
48
+ * 3. Assigns new keys for newly visible items
49
+ * 4. Cleans up excess items to maintain the recycling pool size
50
+ *
51
+ * @param getStableId - Function to get a stable identifier for an item at a specific index
52
+ * @param getItemType - Function to get the type of an item at a specific index
53
+ * @param engagedIndices - Collection of indices that are currently visible or engaged
54
+ * @param dataLength - Total length of the data set
55
+ */
56
+ public sync(
57
+ getStableId: (index: number) => string,
58
+ getItemType: (index: number) => string,
59
+ engagedIndices: ConsecutiveNumbers,
60
+ dataLength: number
61
+ ) {
62
+ this.clearRecyclePool();
63
+ this.unProcessedIndices.clear();
64
+
65
+ // Recycle keys for items that are no longer valid or visible
66
+ this.keyMap.forEach((keyInfo, key) => {
67
+ const { index, stableId, itemType } = keyInfo;
68
+ if (index >= dataLength) {
69
+ this.recycleKey(key);
70
+ return;
71
+ }
72
+ if (!this.disableRecycling) {
73
+ this.unProcessedIndices.add(index);
74
+ }
75
+ if (!engagedIndices.includes(index)) {
76
+ this.recycleKey(key);
77
+ return;
78
+ }
79
+ const newStableId = getStableId(index);
80
+ const newItemType = getItemType(index);
81
+ if (stableId !== newStableId || itemType !== newItemType) {
82
+ this.recycleKey(key);
83
+ }
84
+ });
85
+
86
+ // First pass: process items that already have optimized keys
87
+ for (const index of engagedIndices) {
88
+ if (this.hasOptimizedKey(getStableId(index))) {
89
+ this.syncItem(index, getItemType(index), getStableId(index));
90
+ }
91
+ }
92
+
93
+ // Second pass: process remaining items that need new keys
94
+ for (const index of engagedIndices) {
95
+ if (!this.hasOptimizedKey(getStableId(index))) {
96
+ this.syncItem(index, getItemType(index), getStableId(index));
97
+ }
98
+ }
99
+
100
+ // create indices that are not in the engagedIndices and less than dataLength
101
+ // select only indices that are not in the engagedIndices
102
+ const validIndicesInPool: number[] = [];
103
+ for (const keyInfo of this.keyMap.values()) {
104
+ const index = keyInfo.index;
105
+ if (index < dataLength && !engagedIndices.includes(index)) {
106
+ validIndicesInPool.push(index);
107
+ }
108
+ }
109
+
110
+ // First pass: process items that already have optimized keys
111
+ for (const index of validIndicesInPool) {
112
+ if (this.hasOptimizedKey(getStableId(index))) {
113
+ this.syncItem(index, getItemType(index), getStableId(index));
114
+ }
115
+ }
116
+
117
+ for (const index of validIndicesInPool) {
118
+ if (!this.hasOptimizedKey(getStableId(index))) {
119
+ this.syncItem(index, getItemType(index), getStableId(index));
120
+ }
121
+ }
122
+
123
+ // Clean up stale items and manage the recycle pool size
124
+ this.cleanup(getStableId, getItemType, engagedIndices, dataLength);
125
+ }
126
+
127
+ /**
128
+ * Checks if a stable ID already has an assigned key
129
+ */
130
+ private hasOptimizedKey(stableId: string): boolean {
131
+ return this.stableIdMap.has(stableId);
132
+ }
133
+
134
+ /**
135
+ * Cleans up stale keys and manages the recycle pool size.
136
+ * This ensures we don't maintain references to items that are no longer in the dataset,
137
+ * and limits the number of recycled items to avoid excessive memory usage.
138
+ */
139
+ private cleanup(
140
+ getStableId: (index: number) => string,
141
+ getItemType: (index: number) => string,
142
+ engagedIndices: ConsecutiveNumbers,
143
+ dataLength: number
144
+ ) {
145
+ const itemsToDelete = new Array<string>();
146
+
147
+ // Remove items that are no longer in the dataset
148
+ for (const [key, keyInfo] of this.keyMap.entries()) {
149
+ const { index, itemType, stableId } = keyInfo;
150
+ const indexOutOfBounds = index >= dataLength;
151
+ const hasStableIdChanged =
152
+ !indexOutOfBounds && getStableId(index) !== stableId;
153
+
154
+ if (indexOutOfBounds || hasStableIdChanged) {
155
+ const nextIndex = this.unProcessedIndices.values().next().value;
156
+ let shouldDeleteKey = true;
157
+
158
+ if (nextIndex !== undefined) {
159
+ const nextItemType = getItemType(nextIndex);
160
+ const nextStableId = getStableId(nextIndex);
161
+ if (itemType === nextItemType) {
162
+ this.syncItem(nextIndex, nextItemType, nextStableId);
163
+ shouldDeleteKey = false;
164
+ }
165
+ }
166
+ if (shouldDeleteKey) {
167
+ this.deleteKeyFromRecyclePool(itemType, key);
168
+ this.stableIdMap.delete(stableId);
169
+ itemsToDelete.push(key);
170
+ }
171
+ }
172
+ }
173
+
174
+ for (const key of itemsToDelete) {
175
+ this.keyMap.delete(key);
176
+ }
177
+
178
+ // Limit the size of the recycle pool
179
+ const itemsRenderedForRecycling = this.keyMap.size - engagedIndices.length;
180
+ if (itemsRenderedForRecycling > this.maxItemsInRecyclePool) {
181
+ const deleteCount =
182
+ itemsRenderedForRecycling - this.maxItemsInRecyclePool;
183
+ let deleted = 0;
184
+
185
+ // Use a for loop so we can break early once we've deleted enough items
186
+ const entries = Array.from(this.keyMap.entries()).reverse();
187
+ for (let i = 0; i < entries.length && deleted < deleteCount; i++) {
188
+ const [key, keyInfo] = entries[i];
189
+ const { index, itemType, stableId } = keyInfo;
190
+
191
+ if (!engagedIndices.includes(index)) {
192
+ this.deleteKeyFromRecyclePool(itemType, key);
193
+ this.stableIdMap.delete(stableId);
194
+ this.keyMap.delete(key);
195
+ deleted++;
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Places a key back into its type-specific recycle pool for future reuse
203
+ */
204
+ private recycleKey(key: string): void {
205
+ if (this.disableRecycling) {
206
+ return;
207
+ }
208
+ const keyInfo = this.keyMap.get(key);
209
+
210
+ if (!keyInfo) {
211
+ return;
212
+ }
213
+
214
+ const { itemType } = keyInfo;
215
+
216
+ // Add key back to its type's pool
217
+ const pool = this.getRecyclePoolForType(itemType);
218
+
219
+ pool.add(key);
220
+ }
221
+
222
+ /**
223
+ * Returns the current render stack containing all active keys and their metadata
224
+ */
225
+ public getRenderStack() {
226
+ return this.keyMap;
227
+ }
228
+
229
+ /**
230
+ * Syncs an individual item by assigning it an appropriate key.
231
+ * Will use an existing key if available, or generate a new one.
232
+ *
233
+ * @returns The key assigned to the item
234
+ */
235
+ private syncItem(index: number, itemType: string, stableId: string): string {
236
+ // Try to reuse an existing key, or get one from the recycle pool, or generate a new one
237
+ const newKey =
238
+ this.stableIdMap.get(stableId) ||
239
+ this.getKeyFromRecyclePool(itemType) ||
240
+ this.generateKey();
241
+
242
+ this.unProcessedIndices.delete(index);
243
+
244
+ const keyInfo = this.keyMap.get(newKey);
245
+ if (keyInfo) {
246
+ // Update an existing key's metadata
247
+ this.deleteKeyFromRecyclePool(itemType, newKey);
248
+ this.deleteKeyFromRecyclePool(keyInfo.itemType, newKey);
249
+ this.stableIdMap.delete(keyInfo.stableId);
250
+ keyInfo.index = index;
251
+ keyInfo.itemType = itemType;
252
+ keyInfo.stableId = stableId;
253
+ } else {
254
+ // Create a new entry in the key map
255
+ this.keyMap.set(newKey, {
256
+ itemType,
257
+ index,
258
+ stableId,
259
+ });
260
+ }
261
+ this.stableIdMap.set(stableId, newKey);
262
+
263
+ return newKey;
264
+ }
265
+
266
+ /**
267
+ * Clears all recycled keys from the pool, effectively resetting the recycling system.
268
+ * This operation does not affect currently active keys.
269
+ */
270
+ private clearRecyclePool() {
271
+ // iterate over all pools and clear them
272
+ for (const pool of this.recycleKeyPools.values()) {
273
+ pool.clear();
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Generates a unique sequential key using an internal counter.
279
+ * @returns A unique key as a string
280
+ */
281
+ private generateKey(): string {
282
+ return (this.keyCounter++).toString();
283
+ }
284
+
285
+ /**
286
+ * Removes a specific key from its type's recycle pool
287
+ */
288
+ private deleteKeyFromRecyclePool(itemType: string, key: string) {
289
+ this.recycleKeyPools.get(itemType)?.delete(key);
290
+ }
291
+
292
+ /**
293
+ * Gets or creates a recycle pool for a specific item type
294
+ */
295
+ private getRecyclePoolForType(itemType: string) {
296
+ let pool = this.recycleKeyPools.get(itemType);
297
+ if (!pool) {
298
+ pool = new Set();
299
+ this.recycleKeyPools.set(itemType, pool);
300
+ }
301
+ return pool;
302
+ }
303
+
304
+ /**
305
+ * Retrieves and removes a key from the type's recycle pool
306
+ * @returns A recycled key or undefined if none available
307
+ */
308
+ private getKeyFromRecyclePool(itemType: string) {
309
+ const pool = this.getRecyclePoolForType(itemType);
310
+ if (pool.size > 0) {
311
+ const key = pool.values().next().value;
312
+ pool.delete(key);
313
+ return key;
314
+ }
315
+ return undefined;
316
+ }
317
+ }
@@ -88,7 +88,7 @@ const ViewHolderInternal = <TItem,>(props: ViewHolderProps<TItem>) => {
88
88
  );
89
89
 
90
90
  const separator = useMemo(() => {
91
- return ItemSeparatorComponent ? (
91
+ return ItemSeparatorComponent && trailingItem !== undefined ? (
92
92
  <ItemSeparatorComponent leadingItem={item} trailingItem={trailingItem} />
93
93
  ) : null;
94
94
  }, [ItemSeparatorComponent, item, trailingItem]);
@@ -97,7 +97,10 @@ const ViewHolderInternal = <TItem,>(props: ViewHolderProps<TItem>) => {
97
97
 
98
98
  const children = useMemo(() => {
99
99
  return renderItem?.({ item, index, extraData, target }) ?? null;
100
- }, [item, index, extraData, target, renderItem]);
100
+ // TODO: Test more thoroughly
101
+ // We don't really to re-render the children when the index changes
102
+ // eslint-disable-next-line react-hooks/exhaustive-deps
103
+ }, [item, extraData, target, renderItem]);
101
104
 
102
105
  const style = {
103
106
  flexDirection: horizontal ? "row" : "column",
@@ -110,7 +113,6 @@ const ViewHolderInternal = <TItem,>(props: ViewHolderProps<TItem>) => {
110
113
  maxWidth: layout.maxWidth,
111
114
  left: layout.x,
112
115
  top: layout.y,
113
- zIndex: 0,
114
116
  } as const;
115
117
 
116
118
  // TODO: Fix this type issue
@@ -21,7 +21,7 @@ export interface ViewHolderCollectionProps<TItem> {
21
21
  /** The data array to be rendered */
22
22
  data: FlashListProps<TItem>["data"];
23
23
  /** Map of indices to React keys for each rendered item */
24
- renderStack: Map<number, string>;
24
+ renderStack: Map<string, { index: number }>;
25
25
  /** Function to get layout information for a specific index */
26
26
  getLayout: (index: number) => RVLayout;
27
27
  /** Ref to control layout updates from parent components */
@@ -37,15 +37,19 @@ export interface ViewHolderCollectionProps<TItem> {
37
37
  /** Function to get the container's layout dimensions */
38
38
  getChildContainerLayout: () => RVDimension | undefined;
39
39
  /** Callback after layout effects are committed */
40
- onCommitLayoutEffect?: () => void;
40
+ onCommitLayoutEffect: () => void;
41
41
  /** Callback after effects are committed */
42
- onCommitEffect?: () => void;
42
+ onCommitEffect: () => void;
43
43
  /** Optional custom component to wrap each item */
44
44
  CellRendererComponent?: FlashListProps<TItem>["CellRendererComponent"];
45
45
  /** Optional component to render between items */
46
46
  ItemSeparatorComponent?: FlashListProps<TItem>["ItemSeparatorComponent"];
47
47
  /** Whether the list is horizontal or vertical */
48
48
  horizontal: FlashListProps<TItem>["horizontal"];
49
+ /** Function to get the adjustment margin for the container.
50
+ * For startRenderingFromBottom, we need to adjust the height of the container
51
+ */
52
+ getAdjustmentMargin: () => number;
49
53
  }
50
54
 
51
55
  /**
@@ -79,6 +83,7 @@ export const ViewHolderCollection = <TItem,>(
79
83
  ItemSeparatorComponent,
80
84
  onCommitEffect,
81
85
  horizontal,
86
+ getAdjustmentMargin,
82
87
  } = props;
83
88
 
84
89
  const [renderId, setRenderId] = React.useState(0);
@@ -99,43 +104,66 @@ export const ViewHolderCollection = <TItem,>(
99
104
  // );
100
105
  recyclerViewContext?.layout();
101
106
  }
107
+ // we need to run this callback on when fixedContainerSize changes
108
+ // eslint-disable-next-line react-hooks/exhaustive-deps
102
109
  }, [fixedContainerSize]);
103
110
 
104
111
  useLayoutEffect(() => {
105
112
  if (renderId > 0) {
106
113
  onCommitLayoutEffect?.();
107
114
  }
115
+ // we need to run this callback on when renderId changes
116
+ // eslint-disable-next-line react-hooks/exhaustive-deps
108
117
  }, [renderId]);
109
118
 
110
119
  useEffect(() => {
111
120
  if (renderId > 0) {
112
121
  onCommitEffect?.();
113
122
  }
123
+ // we need to run this callback on when renderId changes
124
+ // eslint-disable-next-line react-hooks/exhaustive-deps
114
125
  }, [renderId]);
115
126
 
116
127
  // Expose forceUpdate through ref
117
- useImperativeHandle(viewHolderCollectionRef, () => ({
118
- commitLayout: () => {
119
- // This will trigger a re-render of the component
120
- setRenderId((prev) => prev + 1);
121
- },
122
- }));
128
+ useImperativeHandle(
129
+ viewHolderCollectionRef,
130
+ () => ({
131
+ commitLayout: () => {
132
+ // This will trigger a re-render of the component
133
+ setRenderId((prev) => prev + 1);
134
+ },
135
+ }),
136
+ [setRenderId]
137
+ );
123
138
 
124
139
  const hasData = data && data.length > 0;
125
140
 
126
141
  const containerStyle = {
127
142
  width: horizontal ? containerLayout?.width : undefined,
128
143
  height: containerLayout?.height,
144
+ marginTop: horizontal ? undefined : getAdjustmentMargin(),
145
+ marginLeft: horizontal ? getAdjustmentMargin() : undefined,
146
+ // TODO: Temp workaround, useLayoutEffect doesn't block paint in some cases
147
+ // We need to investigate why this is happening
148
+ opacity: renderId > 0 ? 1 : 0,
129
149
  };
130
150
 
151
+ // sort by index and log
152
+ // const sortedRenderStack = Array.from(renderStack.entries()).sort(
153
+ // ([, a], [, b]) => a.index - b.index
154
+ // );
155
+ // console.log(
156
+ // "sortedRenderStack",
157
+ // sortedRenderStack.map(([reactKey, { index }]) => {
158
+ // return `${index} => ${reactKey}`;
159
+ // })
160
+ // );
161
+
131
162
  return (
132
- <CompatView
133
- // TODO: Take care of web scroll bar here
134
- style={hasData && containerStyle}
135
- >
163
+ <CompatView style={hasData && containerStyle}>
136
164
  {containerLayout &&
137
165
  hasData &&
138
- Array.from(renderStack, ([index, reactKey]) => {
166
+ Array.from(renderStack.entries(), ([reactKey, { index }]) => {
139
167
  const item = data[index];
140
168
  const trailingItem = ItemSeparatorComponent
141
169
  ? data[index + 1]
@@ -15,6 +15,7 @@ import { CompatView } from "./CompatView";
15
15
  export interface ScrollAnchorProps {
16
16
  /** Ref to access scroll anchor methods */
17
17
  scrollAnchorRef: React.Ref<ScrollAnchorRef>;
18
+ horizontal: boolean;
18
19
  }
19
20
 
20
21
  /**
@@ -27,28 +28,39 @@ export interface ScrollAnchorRef {
27
28
 
28
29
  /**
29
30
  * ScrollAnchor component that provides programmatic scrolling capabilities using maintainVisibleContentPosition property
30
- * TODO: Implement this for web
31
31
  * @param props - Component props
32
32
  * @returns An invisible anchor element used for scrolling
33
33
  */
34
- export function ScrollAnchor({ scrollAnchorRef }: ScrollAnchorProps) {
34
+ export function ScrollAnchor({
35
+ scrollAnchorRef,
36
+ horizontal,
37
+ }: ScrollAnchorProps) {
35
38
  const [scrollOffset, setScrollOffset] = useState(1000000); // TODO: Fix this value
36
39
 
37
40
  // Expose scrollBy method through ref
38
- useImperativeHandle(scrollAnchorRef, () => ({
39
- scrollBy: (offset: number) => {
40
- setScrollOffset((prev) => prev + offset);
41
- },
42
- }));
41
+ useImperativeHandle(
42
+ scrollAnchorRef,
43
+ () => ({
44
+ scrollBy: (offset: number) => {
45
+ setScrollOffset((prev) => prev + offset);
46
+ },
47
+ }),
48
+ []
49
+ );
43
50
 
44
51
  // Create an invisible anchor element that can be positioned
45
52
  const anchor = useMemo(() => {
46
53
  return (
47
54
  <CompatView
48
- style={{ position: "absolute", height: 0, top: scrollOffset }}
55
+ style={{
56
+ position: "absolute",
57
+ height: 0,
58
+ top: horizontal ? 0 : scrollOffset,
59
+ left: horizontal ? scrollOffset : 0,
60
+ }}
49
61
  />
50
62
  );
51
- }, [scrollOffset]);
63
+ }, [scrollOffset, horizontal]);
52
64
 
53
65
  return anchor;
54
66
  }
@@ -49,6 +49,11 @@ export interface StickyHeaderRef {
49
49
  reportScrollEvent: (event: NativeScrollEvent) => void;
50
50
  }
51
51
 
52
+ interface StickyHeaderState {
53
+ currentStickyIndex: number;
54
+ pushStartsAt: number;
55
+ }
56
+
52
57
  export const StickyHeaders = <TItem,>({
53
58
  stickyHeaderIndices,
54
59
  renderItem,
@@ -58,25 +63,35 @@ export const StickyHeaders = <TItem,>({
58
63
  data,
59
64
  extraData,
60
65
  }: StickyHeaderProps<TItem>) => {
61
- const [stickyIndices, setStickyIndices] = useState<{
62
- currentStickyIndex: number;
63
- nextStickyIndex: number;
64
- }>({ currentStickyIndex: -1, nextStickyIndex: -1 });
66
+ const [stickyHeaderState, setStickyHeaderState] = useState<StickyHeaderState>(
67
+ {
68
+ currentStickyIndex: -1,
69
+ pushStartsAt: Number.MAX_SAFE_INTEGER,
70
+ }
71
+ );
65
72
 
66
- const { currentStickyIndex, nextStickyIndex } = stickyIndices;
73
+ const { currentStickyIndex, pushStartsAt } = stickyHeaderState;
67
74
 
68
- // Memoize sorted indices based on their Y positions
75
+ // sort indices and memoize compute
69
76
  const sortedIndices = useMemo(() => {
70
- return stickyHeaderIndices.sort((first, second) => first - second);
77
+ return [...stickyHeaderIndices].sort((first, second) => first - second);
71
78
  }, [stickyHeaderIndices]);
72
79
 
80
+ const legthInvalid =
81
+ sortedIndices.length === 0 ||
82
+ recyclerViewManager.getDataLength() <=
83
+ sortedIndices[sortedIndices.length - 1];
84
+
73
85
  const compute = useCallback(() => {
74
- const adjustedValue = recyclerViewManager.getLastScrollOffset();
86
+ if (legthInvalid) {
87
+ return;
88
+ }
89
+ const adjustedScrollOffset = recyclerViewManager.getLastScrollOffset();
75
90
 
76
91
  // Binary search for current sticky index
77
92
  const currentIndexInArray = findCurrentStickyIndex(
78
93
  sortedIndices,
79
- adjustedValue,
94
+ adjustedScrollOffset,
80
95
  (index) => recyclerViewManager.getLayout(index).y
81
96
  );
82
97
 
@@ -87,16 +102,33 @@ export const StickyHeaders = <TItem,>({
87
102
  newNextStickyIndex = -1;
88
103
  }
89
104
 
105
+ // To make sure header offset is 0 in the interpolate compute
106
+ const newNextStickyY =
107
+ newNextStickyIndex === -1
108
+ ? Number.MAX_SAFE_INTEGER
109
+ : (recyclerViewManager.tryGetLayout(newNextStickyIndex)?.y ?? 0) +
110
+ recyclerViewManager.firstItemOffset;
111
+ const newCurrentStickyHeight =
112
+ recyclerViewManager.tryGetLayout(newStickyIndex)?.height ?? 0;
113
+
114
+ const newPushStartsAt = newNextStickyY - newCurrentStickyHeight;
115
+
90
116
  if (
91
117
  newStickyIndex !== currentStickyIndex ||
92
- newNextStickyIndex !== nextStickyIndex
118
+ newPushStartsAt !== pushStartsAt
93
119
  ) {
94
- setStickyIndices({
120
+ setStickyHeaderState({
95
121
  currentStickyIndex: newStickyIndex,
96
- nextStickyIndex: newNextStickyIndex,
122
+ pushStartsAt: newPushStartsAt,
97
123
  });
98
124
  }
99
- }, [currentStickyIndex, nextStickyIndex, recyclerViewManager, sortedIndices]);
125
+ }, [
126
+ legthInvalid,
127
+ recyclerViewManager,
128
+ sortedIndices,
129
+ currentStickyIndex,
130
+ pushStartsAt,
131
+ ]);
100
132
 
101
133
  useEffect(() => {
102
134
  compute();
@@ -115,35 +147,19 @@ export const StickyHeaders = <TItem,>({
115
147
 
116
148
  const refHolder = useRef(new Map()).current;
117
149
 
118
- // Memoize translateY calculation
119
150
  const translateY = useMemo(() => {
120
- if (currentStickyIndex === -1 || nextStickyIndex === -1) {
121
- return scrollY.interpolate({
122
- inputRange: [0, Infinity],
123
- outputRange: [0, 0],
124
- extrapolate: "clamp",
125
- });
126
- }
127
-
128
- const currentLayout = recyclerViewManager.getLayout(currentStickyIndex);
129
- const nextLayout = recyclerViewManager.getLayout(nextStickyIndex);
130
-
131
- const pushStartsAt = nextLayout.y - currentLayout.height;
151
+ const currentStickyHeight =
152
+ recyclerViewManager.tryGetLayout(currentStickyIndex)?.height ?? 0;
132
153
 
133
154
  return scrollY.interpolate({
134
- inputRange: [
135
- pushStartsAt + recyclerViewManager.firstItemOffset,
136
- nextLayout.y + recyclerViewManager.firstItemOffset,
137
- ],
138
- outputRange: [0, -currentLayout.height],
155
+ inputRange: [pushStartsAt, pushStartsAt + currentStickyHeight],
156
+ outputRange: [0, -currentStickyHeight],
139
157
  extrapolate: "clamp",
140
158
  });
141
- }, [currentStickyIndex, nextStickyIndex, recyclerViewManager, scrollY]);
159
+ }, [recyclerViewManager, currentStickyIndex, scrollY, pushStartsAt]);
142
160
 
143
161
  // Memoize header content
144
162
  const headerContent = useMemo(() => {
145
- if (currentStickyIndex === -1) return null;
146
-
147
163
  return (
148
164
  <CompatAnimatedView
149
165
  style={{
@@ -155,19 +171,21 @@ export const StickyHeaders = <TItem,>({
155
171
  transform: [{ translateY }],
156
172
  }}
157
173
  >
158
- <ViewHolder
159
- index={currentStickyIndex}
160
- item={data[currentStickyIndex]}
161
- renderItem={renderItem}
162
- layout={{ x: 0, y: 0, width: 0, height: 0 }}
163
- refHolder={refHolder}
164
- extraData={extraData}
165
- trailingItem={null}
166
- target="StickyHeader"
167
- />
174
+ {currentStickyIndex !== -1 && currentStickyIndex < data.length ? (
175
+ <ViewHolder
176
+ index={currentStickyIndex}
177
+ item={data[currentStickyIndex]}
178
+ renderItem={renderItem}
179
+ layout={{ x: 0, y: 0, width: 0, height: 0 }}
180
+ refHolder={refHolder}
181
+ extraData={extraData}
182
+ trailingItem={null}
183
+ target="StickyHeader"
184
+ />
185
+ ) : null}
168
186
  </CompatAnimatedView>
169
187
  );
170
- }, [currentStickyIndex, data, renderItem, extraData, refHolder, translateY]);
188
+ }, [translateY, currentStickyIndex, data, renderItem, refHolder, extraData]);
171
189
 
172
190
  return headerContent;
173
191
  };