@shopify/flash-list 2.0.0-alpha.10 → 2.0.0-alpha.12

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 (121) hide show
  1. package/README.md +6 -2
  2. package/dist/AnimatedFlashList.d.ts.map +1 -1
  3. package/dist/AnimatedFlashList.js +3 -3
  4. package/dist/AnimatedFlashList.js.map +1 -1
  5. package/dist/FlashList.d.ts +9 -0
  6. package/dist/FlashList.d.ts.map +1 -1
  7. package/dist/FlashList.js +20 -0
  8. package/dist/FlashList.js.map +1 -1
  9. package/dist/FlashListProps.d.ts +13 -6
  10. package/dist/FlashListProps.d.ts.map +1 -1
  11. package/dist/FlashListProps.js.map +1 -1
  12. package/dist/FlashListRef.d.ts +305 -0
  13. package/dist/FlashListRef.d.ts.map +1 -0
  14. package/dist/FlashListRef.js +3 -0
  15. package/dist/FlashListRef.js.map +1 -0
  16. package/dist/__tests__/RecyclerView.test.js +62 -27
  17. package/dist/__tests__/RecyclerView.test.js.map +1 -1
  18. package/dist/__tests__/RenderStackManager.test.d.ts +2 -0
  19. package/dist/__tests__/RenderStackManager.test.d.ts.map +1 -0
  20. package/dist/__tests__/RenderStackManager.test.js +405 -0
  21. package/dist/__tests__/RenderStackManager.test.js.map +1 -0
  22. package/dist/__tests__/useUnmountAwareCallbacks.test.js +1 -1
  23. package/dist/__tests__/useUnmountAwareCallbacks.test.js.map +1 -1
  24. package/dist/benchmark/useFlatListBenchmark.js +8 -7
  25. package/dist/benchmark/useFlatListBenchmark.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/recyclerview/RecyclerView.d.ts +2 -1
  30. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  31. package/dist/recyclerview/RecyclerView.js +39 -21
  32. package/dist/recyclerview/RecyclerView.js.map +1 -1
  33. package/dist/recyclerview/RecyclerViewContextProvider.d.ts +6 -5
  34. package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -1
  35. package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -1
  36. package/dist/recyclerview/RecyclerViewManager.d.ts +14 -7
  37. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
  38. package/dist/recyclerview/RecyclerViewManager.js +71 -102
  39. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  40. package/dist/recyclerview/RenderStackManager.d.ts +85 -0
  41. package/dist/recyclerview/RenderStackManager.d.ts.map +1 -0
  42. package/dist/recyclerview/RenderStackManager.js +261 -0
  43. package/dist/recyclerview/RenderStackManager.js.map +1 -0
  44. package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
  45. package/dist/recyclerview/ViewHolder.js +5 -3
  46. package/dist/recyclerview/ViewHolder.js.map +1 -1
  47. package/dist/recyclerview/ViewHolderCollection.d.ts +3 -1
  48. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
  49. package/dist/recyclerview/ViewHolderCollection.js +19 -3
  50. package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
  51. package/dist/recyclerview/components/ScrollAnchor.d.ts +2 -1
  52. package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -1
  53. package/dist/recyclerview/components/ScrollAnchor.js +9 -4
  54. package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
  55. package/dist/recyclerview/components/StickyHeaders.d.ts +1 -1
  56. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
  57. package/dist/recyclerview/components/StickyHeaders.js +39 -32
  58. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  59. package/dist/recyclerview/hooks/useBoundDetection.d.ts +1 -2
  60. package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -1
  61. package/dist/recyclerview/hooks/useBoundDetection.js +19 -16
  62. package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
  63. package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -1
  64. package/dist/recyclerview/hooks/useOnLoad.js +4 -6
  65. package/dist/recyclerview/hooks/useOnLoad.js.map +1 -1
  66. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +3 -48
  67. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  68. package/dist/recyclerview/hooks/useRecyclerViewController.js +111 -77
  69. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  70. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
  71. package/dist/recyclerview/hooks/useRecyclerViewManager.js +6 -0
  72. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
  73. package/dist/recyclerview/hooks/useSecondaryProps.js +1 -1
  74. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts +10 -3
  75. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts.map +1 -1
  76. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js +33 -4
  77. package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js.map +1 -1
  78. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +6 -0
  79. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  80. package/dist/recyclerview/layout-managers/GridLayoutManager.js +27 -5
  81. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  82. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +2 -2
  83. package/dist/recyclerview/layout-managers/LayoutManager.js +2 -2
  84. package/dist/tsconfig.tsbuildinfo +1 -1
  85. package/jestSetup.js +30 -11
  86. package/package.json +1 -1
  87. package/src/AnimatedFlashList.ts +3 -2
  88. package/src/FlashList.tsx +24 -0
  89. package/src/FlashListProps.ts +16 -7
  90. package/src/FlashListRef.ts +320 -0
  91. package/src/__tests__/RecyclerView.test.tsx +83 -29
  92. package/src/__tests__/RenderStackManager.test.ts +488 -0
  93. package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
  94. package/src/benchmark/useFlatListBenchmark.ts +2 -2
  95. package/src/index.ts +1 -0
  96. package/src/recyclerview/RecyclerView.tsx +49 -29
  97. package/src/recyclerview/RecyclerViewContextProvider.ts +12 -6
  98. package/src/recyclerview/RecyclerViewManager.ts +90 -88
  99. package/src/recyclerview/RenderStackManager.ts +265 -0
  100. package/src/recyclerview/ViewHolder.tsx +5 -3
  101. package/src/recyclerview/ViewHolderCollection.tsx +29 -8
  102. package/src/recyclerview/components/ScrollAnchor.tsx +21 -8
  103. package/src/recyclerview/components/StickyHeaders.tsx +62 -44
  104. package/src/recyclerview/hooks/useBoundDetection.ts +25 -18
  105. package/src/recyclerview/hooks/useOnLoad.ts +4 -6
  106. package/src/recyclerview/hooks/useRecyclerViewController.tsx +121 -132
  107. package/src/recyclerview/hooks/useRecyclerViewManager.ts +6 -0
  108. package/src/recyclerview/hooks/useSecondaryProps.tsx +1 -1
  109. package/src/recyclerview/hooks/useUnmountAwareCallbacks.ts +39 -3
  110. package/src/recyclerview/layout-managers/GridLayoutManager.ts +30 -7
  111. package/src/recyclerview/layout-managers/LayoutManager.ts +2 -2
  112. package/dist/__tests__/RecycleKeyManager.test.d.ts +0 -2
  113. package/dist/__tests__/RecycleKeyManager.test.d.ts.map +0 -1
  114. package/dist/__tests__/RecycleKeyManager.test.js +0 -210
  115. package/dist/__tests__/RecycleKeyManager.test.js.map +0 -1
  116. package/dist/recyclerview/RecycleKeyManager.d.ts +0 -82
  117. package/dist/recyclerview/RecycleKeyManager.d.ts.map +0 -1
  118. package/dist/recyclerview/RecycleKeyManager.js +0 -135
  119. package/dist/recyclerview/RecycleKeyManager.js.map +0 -1
  120. package/src/__tests__/RecycleKeyManager.test.ts +0 -254
  121. package/src/recyclerview/RecycleKeyManager.ts +0 -185
@@ -0,0 +1,488 @@
1
+ import { RenderStackManager } from "../recyclerview/RenderStackManager";
2
+ import { ConsecutiveNumbers } from "../recyclerview/helpers/ConsecutiveNumbers";
3
+
4
+ const mock1Data = [
5
+ { id: 1, name: "Item 1", itemType: "type1" },
6
+ { id: 2, name: "Item 2", itemType: "type2" },
7
+ { id: 3, name: "Item 3", itemType: "type1" },
8
+ { id: 4, name: "Item 4", itemType: "type2" },
9
+ { id: 5, name: "Item 5", itemType: "type1" },
10
+ { id: 6, name: "Item 6", itemType: "type2" },
11
+ { id: 7, name: "Item 7", itemType: "type1" },
12
+ { id: 8, name: "Item 8", itemType: "type2" },
13
+ { id: 9, name: "Item 9", itemType: "type1" },
14
+ { id: 10, name: "Item 10", itemType: "type2" },
15
+ { id: 11, name: "Item 11", itemType: "type1" },
16
+ ];
17
+ const mock1 = {
18
+ data: mock1Data,
19
+ getStableId: (index: number) => mock1Data[index].id.toString(),
20
+ getItemType: (index: number) => mock1Data[index].itemType,
21
+ length: mock1Data.length,
22
+ };
23
+
24
+ const mock2Data = [
25
+ { id: 5, name: "Item 1", itemType: "type1" },
26
+ { id: 6, name: "Item 2", itemType: "type2" },
27
+ { id: 7, name: "Item 3", itemType: "type1" },
28
+ { id: 8, name: "Item 4", itemType: "type2" },
29
+ { id: 9, name: "Item 5", itemType: "type1" },
30
+ { id: 10, name: "Item 6", itemType: "type2" },
31
+ { id: 11, name: "Item 7", itemType: "type1" },
32
+ { id: 12, name: "Item 8", itemType: "type2" },
33
+ { id: 13, name: "Item 9", itemType: "type1" },
34
+ { id: 14, name: "Item 10", itemType: "type2" },
35
+ { id: 15, name: "Item 11", itemType: "type1" },
36
+ ];
37
+ const mock2 = {
38
+ data: mock2Data,
39
+ getStableId: (index: number) => mock2Data[index].id.toString(),
40
+ getItemType: (index: number) => mock2Data[index].itemType,
41
+ length: mock2Data.length,
42
+ };
43
+
44
+ const mock3Data = [
45
+ { id: 1, name: "Item 1", itemType: "type1" },
46
+ { id: 2, name: "Item 2", itemType: "type1" },
47
+ { id: 3, name: "Item 3", itemType: "type1" },
48
+ { id: 4, name: "Item 4", itemType: "type1" },
49
+ { id: 5, name: "Item 5", itemType: "type1" },
50
+ { id: 6, name: "Item 6", itemType: "type1" },
51
+ { id: 7, name: "Item 7", itemType: "type1" },
52
+ { id: 8, name: "Item 8", itemType: "type1" },
53
+ { id: 9, name: "Item 9", itemType: "type1" },
54
+ { id: 10, name: "Item 10", itemType: "type1" },
55
+ { id: 11, name: "Item 11", itemType: "type1" },
56
+ { id: 12, name: "Item 12", itemType: "type1" },
57
+ { id: 13, name: "Item 13", itemType: "type1" },
58
+ { id: 14, name: "Item 14", itemType: "type1" },
59
+ { id: 15, name: "Item 15", itemType: "type1" },
60
+ ];
61
+ const mock3 = {
62
+ data: mock3Data,
63
+ getStableId: (index: number) => mock3Data[index].id.toString(),
64
+ getItemType: (index: number) => mock3Data[index].itemType,
65
+ length: mock3Data.length,
66
+ };
67
+
68
+ const mock4Data = [
69
+ { id: 1, name: "Item 1", itemType: "type1" },
70
+ { id: 2, name: "Item 2", itemType: "type1" },
71
+ { id: 3, name: "Item 3", itemType: "type1" },
72
+ { id: 4, name: "Item 4", itemType: "type1" },
73
+ { id: 5, name: "Item 5", itemType: "type1" },
74
+ { id: 6, name: "Item 6", itemType: "type1" },
75
+ { id: 7, name: "Item 7", itemType: "type1" },
76
+ { id: 8, name: "Item 8", itemType: "type2" },
77
+ { id: 9, name: "Item 9", itemType: "type2" },
78
+ { id: 10, name: "Item 10", itemType: "type2" },
79
+ { id: 11, name: "Item 11", itemType: "type2" },
80
+ { id: 12, name: "Item 12", itemType: "type2" },
81
+ { id: 13, name: "Item 13", itemType: "type2" },
82
+ { id: 14, name: "Item 14", itemType: "type2" },
83
+ { id: 15, name: "Item 15", itemType: "type2" },
84
+ ];
85
+ const mock4 = {
86
+ data: mock4Data,
87
+ getStableId: (index: number) => mock4Data[index].id.toString(),
88
+ getItemType: (index: number) => mock4Data[index].itemType,
89
+ length: mock4Data.length,
90
+ };
91
+
92
+ const mock5Data = [
93
+ { id: 1, name: "Item 1", itemType: "type2" },
94
+ { id: 2, name: "Item 2", itemType: "type2" },
95
+ { id: 3, name: "Item 3", itemType: "type2" },
96
+ { id: 4, name: "Item 4", itemType: "type2" },
97
+ { id: 5, name: "Item 5", itemType: "type2" },
98
+ { id: 6, name: "Item 6", itemType: "type2" },
99
+ { id: 7, name: "Item 7", itemType: "type2" },
100
+ { id: 8, name: "Item 8", itemType: "type1" },
101
+ { id: 9, name: "Item 9", itemType: "type1" },
102
+ { id: 10, name: "Item 10", itemType: "type1" },
103
+ { id: 11, name: "Item 11", itemType: "type1" },
104
+ { id: 12, name: "Item 12", itemType: "type1" },
105
+ { id: 13, name: "Item 13", itemType: "type1" },
106
+ { id: 14, name: "Item 14", itemType: "type1" },
107
+ { id: 15, name: "Item 15", itemType: "type1" },
108
+ ];
109
+ const mock5 = {
110
+ data: mock5Data,
111
+ getStableId: (index: number) => mock5Data[index].id.toString(),
112
+ getItemType: (index: number) => mock5Data[index].itemType,
113
+ length: mock5Data.length,
114
+ };
115
+
116
+ const mock6Data = [
117
+ { id: 0, name: "Item 0", itemType: "type1" },
118
+ { id: 1, name: "Item 1", itemType: "type1" },
119
+ { id: 2, name: "Item 2", itemType: "type1" },
120
+ { id: 3, name: "Item 3", itemType: "type1" },
121
+ { id: 4, name: "Item 4", itemType: "type1" },
122
+ { id: 5, name: "Item 5", itemType: "type1" },
123
+ { id: 6, name: "Item 6", itemType: "type1" },
124
+ { id: 7, name: "Item 7", itemType: "type1" },
125
+ ];
126
+ const mock6 = {
127
+ data: mock6Data,
128
+ getStableId: (index: number) => mock6Data[index].id.toString(),
129
+ getItemType: (index: number) => mock6Data[index].itemType,
130
+ length: mock6Data.length,
131
+ };
132
+
133
+ const mock7Data = [
134
+ { id: 0, name: "Item 0", itemType: "type1" },
135
+ { id: 2, name: "Item 2", itemType: "type1" },
136
+ { id: 3, name: "Item 3", itemType: "type1" },
137
+ { id: 4, name: "Item 4", itemType: "type1" },
138
+ { id: 5, name: "Item 5", itemType: "type1" },
139
+ { id: 6, name: "Item 6", itemType: "type1" },
140
+ { id: 7, name: "Item 7", itemType: "type1" },
141
+ { id: 8, name: "Item 8", itemType: "type1" },
142
+ ];
143
+ const mock7 = {
144
+ data: mock7Data,
145
+ getStableId: (index: number) => mock7Data[index].id.toString(),
146
+ getItemType: (index: number) => mock7Data[index].itemType,
147
+ length: mock7Data.length,
148
+ };
149
+
150
+ // Helper to create mock data structures
151
+ const createMockData = (
152
+ items: { id: string | number; itemType: string; name?: string }[]
153
+ ) => {
154
+ return {
155
+ data: items.map((item) => ({
156
+ ...item,
157
+ name: item.name || `Item ${item.id}`,
158
+ })),
159
+ getStableId: (index: number) => items[index].id.toString(),
160
+ getItemType: (index: number) => items[index].itemType,
161
+ length: items.length,
162
+ };
163
+ };
164
+
165
+ // Helper to run sync and get sorted keys from the entire keyMap
166
+ const runSyncAndGetEntireKeyMapKeys = (
167
+ manager: RenderStackManager,
168
+ mock: {
169
+ data: any[];
170
+ getStableId: (index: number) => string;
171
+ getItemType: (index: number) => string;
172
+ length: number;
173
+ },
174
+ engagedIndicesOverride?: ConsecutiveNumbers
175
+ ) => {
176
+ const dataLength = mock.length;
177
+ const engaged =
178
+ engagedIndicesOverride ??
179
+ new ConsecutiveNumbers(0, dataLength > 0 ? dataLength - 1 : -1);
180
+ manager.sync(mock.getStableId, mock.getItemType, engaged, dataLength);
181
+ return Array.from(manager.getRenderStack().keys()).sort(
182
+ (keyA, keyB) => Number(keyA) - Number(keyB)
183
+ );
184
+ };
185
+
186
+ // Helper to get keys specific to the items in a mock, after a sync
187
+ const getKeysForMockItems = (
188
+ manager: RenderStackManager,
189
+ mockData: {
190
+ data: { id: any }[];
191
+ getStableId: (index: number) => string;
192
+ length: number;
193
+ }
194
+ ) => {
195
+ const stack = manager.getRenderStack();
196
+ const keys = [];
197
+ // Ensure we only try to get keys for items that exist in mockData
198
+ for (let i = 0; i < mockData.length; i++) {
199
+ const stableId = mockData.getStableId(i);
200
+ for (const [key, info] of stack.entries()) {
201
+ if (info.stableId === stableId) {
202
+ keys.push(key);
203
+ break;
204
+ }
205
+ }
206
+ }
207
+ return keys.sort((keyA, keyB) => Number(keyA) - Number(keyB));
208
+ };
209
+
210
+ const emptyMock = createMockData([]);
211
+ const mockDataA5 = createMockData([
212
+ { id: "s1", itemType: "typeA" },
213
+ { id: "s2", itemType: "typeA" },
214
+ { id: "s3", itemType: "typeA" },
215
+ { id: "s4", itemType: "typeA" },
216
+ { id: "s5", itemType: "typeA" },
217
+ ]);
218
+ const mockDataB3 = createMockData([
219
+ { id: "s6", itemType: "typeA" },
220
+ { id: "s7", itemType: "typeA" },
221
+ { id: "s8", itemType: "typeA" },
222
+ ]);
223
+
224
+ describe("RenderStackManager", () => {
225
+ it("should reuse keys from removed items when transitioning from mock1 to mock2", () => {
226
+ const renderStackManager = new RenderStackManager();
227
+ runSyncAndGetEntireKeyMapKeys(renderStackManager, mock1);
228
+ const oldRenderStackKeys = Array.from(
229
+ renderStackManager.getRenderStack().keys()
230
+ ).sort((keyA, keyB) => Number(keyA) - Number(keyB));
231
+
232
+ runSyncAndGetEntireKeyMapKeys(renderStackManager, mock2);
233
+ const newRenderStackKeys = Array.from(
234
+ renderStackManager.getRenderStack().keys()
235
+ ).sort((keyA, keyB) => Number(keyA) - Number(keyB));
236
+ expect(newRenderStackKeys).toEqual(oldRenderStackKeys);
237
+ });
238
+
239
+ it("should reuse keys changing item types when transitioning from mock3 to mock4", () => {
240
+ const renderStackManager = new RenderStackManager();
241
+ runSyncAndGetEntireKeyMapKeys(renderStackManager, mock3);
242
+ const oldRenderStackKeys = Array.from(
243
+ renderStackManager.getRenderStack().keys()
244
+ ).sort((keyA, keyB) => Number(keyA) - Number(keyB));
245
+
246
+ runSyncAndGetEntireKeyMapKeys(renderStackManager, mock4);
247
+ const newRenderStackKeys = Array.from(
248
+ renderStackManager.getRenderStack().keys()
249
+ ).sort((keyA, keyB) => Number(keyA) - Number(keyB));
250
+ expect(newRenderStackKeys).toEqual(oldRenderStackKeys);
251
+ });
252
+
253
+ it("should reuse keys changing item types when transitioning from mock4 to mock5", () => {
254
+ const renderStackManager = new RenderStackManager();
255
+ runSyncAndGetEntireKeyMapKeys(renderStackManager, mock4);
256
+ const oldRenderStackKeys = Array.from(
257
+ renderStackManager.getRenderStack().keys()
258
+ ).sort((keyA, keyB) => Number(keyA) - Number(keyB));
259
+
260
+ runSyncAndGetEntireKeyMapKeys(renderStackManager, mock5);
261
+ const newRenderStackKeys = Array.from(
262
+ renderStackManager.getRenderStack().keys()
263
+ ).sort((keyA, keyB) => Number(keyA) - Number(keyB));
264
+ expect(newRenderStackKeys).toEqual(oldRenderStackKeys);
265
+ });
266
+
267
+ it("should have all keys from mock1 when going from mock1 to mock5", () => {
268
+ const renderStackManager = new RenderStackManager();
269
+ runSyncAndGetEntireKeyMapKeys(renderStackManager, mock1);
270
+ const oldRenderStackKeys = Array.from(
271
+ renderStackManager.getRenderStack().keys()
272
+ ).sort((keyA, keyB) => Number(keyA) - Number(keyB));
273
+
274
+ runSyncAndGetEntireKeyMapKeys(renderStackManager, mock5);
275
+ const newRenderStackKeys = Array.from(
276
+ renderStackManager.getRenderStack().keys()
277
+ ).sort((keyA, keyB) => Number(keyA) - Number(keyB));
278
+
279
+ oldRenderStackKeys.forEach((key) => {
280
+ expect(newRenderStackKeys).toContain(key);
281
+ });
282
+ });
283
+ });
284
+
285
+ describe("RenderStackManager with disableRecycling = true", () => {
286
+ it("should assign new, non-recycled keys to new items when disableRecycling is true", () => {
287
+ const rsm = new RenderStackManager();
288
+ rsm.disableRecycling = true;
289
+
290
+ // Sync with A5 first
291
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataA5);
292
+ const keysA5 = getKeysForMockItems(rsm, mockDataA5);
293
+ expect(keysA5).toEqual(["0", "1", "2", "3", "4"]);
294
+
295
+ // Sync with B3
296
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataB3);
297
+ const keysB3 = getKeysForMockItems(rsm, mockDataB3);
298
+ expect(keysB3).toEqual(["5", "6", "7"]); // New keys for B3 items
299
+
300
+ // Ensure B3 keys don't overlap with A5 keys that might remain in keyMap
301
+ keysA5.forEach((keyA) => {
302
+ expect(keysB3).not.toContain(keyA);
303
+ });
304
+
305
+ // Check the final state of the entire keyMap
306
+ // After B3 sync, keys for A5 items at original indices 3,4 (stableIds "s4","s5")
307
+ // should be removed because 3 >= B3.length (3) and 4 >= B3.length (3). Keys for 0,1,2 from A5 remain.
308
+ const allKeysInMap = runSyncAndGetEntireKeyMapKeys(rsm, mockDataB3); // This re-syncs B3, ensuring state is for B3
309
+ expect(
310
+ allKeysInMap.sort((keyA, keyB) => Number(keyA) - Number(keyB))
311
+ ).toEqual(["5", "6", "7"]);
312
+ });
313
+
314
+ it("should generate all new keys if starting with disableRecycling = true and items are removed then added", () => {
315
+ const rsm = new RenderStackManager();
316
+ rsm.disableRecycling = true;
317
+
318
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataA5); // Assigns keys "0" through "4"
319
+ runSyncAndGetEntireKeyMapKeys(rsm, emptyMock); // Sync with empty
320
+ expect(getKeysForMockItems(rsm, emptyMock)).toEqual([]);
321
+
322
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataB3); // Sync with new data
323
+ const keysForNewItems = getKeysForMockItems(rsm, mockDataB3);
324
+ expect(keysForNewItems).toEqual(["5", "6", "7"]);
325
+ });
326
+ });
327
+
328
+ describe("RenderStackManager with maxItemsInRecyclePool", () => {
329
+ it("should not recycle any keys when maxItemsInRecyclePool is 0", () => {
330
+ const rsm = new RenderStackManager(0); // maxItemsInRecyclePool = 0
331
+
332
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataA5);
333
+ runSyncAndGetEntireKeyMapKeys(rsm, emptyMock); // Sync with empty, dataLength = 0. All keys are cleaned up.
334
+
335
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataB3);
336
+ const keys2 = getKeysForMockItems(rsm, mockDataB3);
337
+ expect(keys2).toEqual(["5", "6", "7"]); // Expect new keys as pool was cleared by emptyMock sync
338
+ });
339
+
340
+ it("should effectively not recycle if intermediate sync has dataLength 0, regardless of maxPoolSize", () => {
341
+ const maxPoolSize = 2;
342
+ const rsm = new RenderStackManager(maxPoolSize);
343
+
344
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataA5);
345
+ runSyncAndGetEntireKeyMapKeys(rsm, emptyMock); // Sync with empty, dataLength = 0. All keys are cleaned up from pool and map.
346
+
347
+ const mockDataA3NewIds = createMockData([
348
+ { id: "s10", itemType: "typeA" },
349
+ { id: "s11", itemType: "typeA" },
350
+ { id: "s12", itemType: "typeA" },
351
+ ]);
352
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataA3NewIds);
353
+ const newKeys = getKeysForMockItems(rsm, mockDataA3NewIds);
354
+ // Because emptyMock sync (dataLength=0) clears all keys, these will be new.
355
+ expect(newKeys).toEqual(["5", "6", "7"]);
356
+ });
357
+ it("should not repeat index when going from mock6 to mock7", () => {
358
+ const rsm = new RenderStackManager();
359
+ rsm.disableRecycling = true;
360
+ runSyncAndGetEntireKeyMapKeys(rsm, mock6);
361
+ runSyncAndGetEntireKeyMapKeys(rsm, mock7);
362
+ const set = new Set<number>();
363
+ Array.from(rsm.getRenderStack().entries()).forEach(([key, info]) => {
364
+ expect(set.has(info.index)).toBe(false);
365
+ set.add(info.index);
366
+ });
367
+ });
368
+ });
369
+
370
+ describe("RenderStackManager edge cases", () => {
371
+ it("should handle initial sync with empty data and then add items", () => {
372
+ const rsm = new RenderStackManager();
373
+ runSyncAndGetEntireKeyMapKeys(rsm, emptyMock);
374
+ expect(getKeysForMockItems(rsm, emptyMock)).toEqual([]);
375
+
376
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataA5);
377
+ expect(getKeysForMockItems(rsm, mockDataA5)).toEqual([
378
+ "0",
379
+ "1",
380
+ "2",
381
+ "3",
382
+ "4",
383
+ ]);
384
+ });
385
+
386
+ it("should generate new keys if all items removed (synced with empty) and then different items added", () => {
387
+ const rsm = new RenderStackManager(); // Default large pool size
388
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataA5);
389
+ runSyncAndGetEntireKeyMapKeys(rsm, emptyMock); // Sync with empty, dataLength = 0. All keys are cleaned up.
390
+
391
+ const mockDataA3NewIds = createMockData([
392
+ { id: "s10", itemType: "typeA" },
393
+ { id: "s11", itemType: "typeA" },
394
+ { id: "s12", itemType: "typeA" },
395
+ ]);
396
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataA3NewIds);
397
+ const newKeys = getKeysForMockItems(rsm, mockDataA3NewIds);
398
+ // Expect new keys as the emptyMock sync (dataLength=0) cleared the pool and map.
399
+ expect(newKeys).toEqual(["5", "6", "7"]);
400
+ });
401
+
402
+ it("should use new keys if types change completely and no compatible recycled keys exist (after empty sync)", () => {
403
+ const rsm = new RenderStackManager();
404
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataA5);
405
+ runSyncAndGetEntireKeyMapKeys(rsm, emptyMock); // Clear with empty sync
406
+
407
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataB3);
408
+ const keysTypeB = getKeysForMockItems(rsm, mockDataB3);
409
+ expect(keysTypeB).toEqual(["5", "6", "7"]); // Should be new keys after empty sync
410
+
411
+ const mockSingleTypeA = createMockData([{ id: "s20", itemType: "typeA" }]);
412
+ runSyncAndGetEntireKeyMapKeys(rsm, mockSingleTypeA);
413
+ const keyForS20 = getKeysForMockItems(rsm, mockSingleTypeA);
414
+ // After empty sync and B3 sync, A's pool is gone. Key counter is at 8.
415
+ expect(keyForS20).toEqual(["5"]);
416
+ });
417
+
418
+ it("should maintain keys if data and engaged indices do not change", () => {
419
+ const rsm = new RenderStackManager();
420
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataA5);
421
+ const keys1 = getKeysForMockItems(rsm, mockDataA5);
422
+
423
+ runSyncAndGetEntireKeyMapKeys(rsm, mockDataA5);
424
+ const keys2 = getKeysForMockItems(rsm, mockDataA5);
425
+ expect(keys2).toEqual(keys1);
426
+ });
427
+
428
+ it("should correctly handle partial replacement of items, reusing keys for stable items and recycling for replaced ones", () => {
429
+ const rsm = new RenderStackManager();
430
+ const initialMock = createMockData([
431
+ { id: "s1", itemType: "typeA" },
432
+ { id: "s2", itemType: "typeA" },
433
+ { id: "s3", itemType: "typeA" },
434
+ { id: "s4", itemType: "typeA" },
435
+ ]);
436
+ runSyncAndGetEntireKeyMapKeys(rsm, initialMock);
437
+ const initialKeyMap = new Map<string, string>();
438
+ // Populate initialKeyMap correctly using getKeysForMockItems and stable IDs
439
+ initialMock.data.forEach((itemData, index) => {
440
+ // Get the keys for the initialMock items AFTER the sync.
441
+ const currentKeysForInitialMock = getKeysForMockItems(rsm, initialMock);
442
+ const key = currentKeysForInitialMock[index]; // Assumes keys are in order of data
443
+ if (key !== undefined) {
444
+ // Ensure key exists before setting
445
+ initialKeyMap.set(itemData.id.toString(), key);
446
+ }
447
+ });
448
+
449
+ const keyForS1 = initialKeyMap.get("s1")!;
450
+ const keyForS2 = initialKeyMap.get("s2")!;
451
+ const keyForS3 = initialKeyMap.get("s3")!;
452
+ const keyForS4 = initialKeyMap.get("s4")!;
453
+
454
+ const partiallyReplacedMock = createMockData([
455
+ { id: "s1", itemType: "typeA" },
456
+ { id: "s5", itemType: "typeA" },
457
+ { id: "s6", itemType: "typeA" },
458
+ { id: "s4", itemType: "typeA" },
459
+ ]);
460
+ runSyncAndGetEntireKeyMapKeys(rsm, partiallyReplacedMock);
461
+ const finalKeyMap = new Map<string, string>();
462
+ partiallyReplacedMock.data.forEach((itemData, index) => {
463
+ // Get keys for partiallyReplacedMock items AFTER the sync.
464
+ const currentKeysForPartialMock = getKeysForMockItems(
465
+ rsm,
466
+ partiallyReplacedMock
467
+ );
468
+ const key = currentKeysForPartialMock[index]; // Assumes keys are in order
469
+ if (key !== undefined) {
470
+ // Ensure key exists
471
+ finalKeyMap.set(itemData.id.toString(), key);
472
+ }
473
+ });
474
+
475
+ expect(finalKeyMap.get("s1")).toBe(keyForS1);
476
+ expect(finalKeyMap.get("s4")).toBe(keyForS4);
477
+ expect([finalKeyMap.get("s5"), finalKeyMap.get("s6")]).toEqual(
478
+ expect.arrayContaining([keyForS2, keyForS3])
479
+ );
480
+ expect(finalKeyMap.get("s5")).not.toBe(finalKeyMap.get("s6"));
481
+
482
+ const finalKeysForCurrentItems = getKeysForMockItems(
483
+ rsm,
484
+ partiallyReplacedMock
485
+ );
486
+ expect(finalKeysForCurrentItems).toEqual(["0", "1", "2", "3"]);
487
+ });
488
+ });
@@ -1,14 +1,14 @@
1
1
  import React from "react";
2
2
  import { render } from "@quilted/react-testing";
3
3
 
4
- import { useUnmountAwareCallbacks } from "../recyclerview/hooks/useUnmountAwareCallbacks";
4
+ import { useUnmountAwareTimeout } from "../recyclerview/hooks/useUnmountAwareCallbacks";
5
5
 
6
6
  const TestComponent = ({
7
7
  onRender,
8
8
  }: {
9
- onRender: (api: ReturnType<typeof useUnmountAwareCallbacks>) => void;
9
+ onRender: (api: ReturnType<typeof useUnmountAwareTimeout>) => void;
10
10
  }) => {
11
- const api = useUnmountAwareCallbacks();
11
+ const api = useUnmountAwareTimeout();
12
12
  onRender(api);
13
13
  return null;
14
14
  };
@@ -24,7 +24,7 @@ describe("useUnmountAwareCallbacks", () => {
24
24
  });
25
25
 
26
26
  it("returns a setTimeout function", () => {
27
- let api: ReturnType<typeof useUnmountAwareCallbacks> | undefined;
27
+ let api: ReturnType<typeof useUnmountAwareTimeout> | undefined;
28
28
  render(
29
29
  <TestComponent
30
30
  onRender={(hookApi) => {
@@ -40,7 +40,7 @@ describe("useUnmountAwareCallbacks", () => {
40
40
 
41
41
  it("executes the callback after the specified delay", () => {
42
42
  const callback = jest.fn();
43
- let api: ReturnType<typeof useUnmountAwareCallbacks> | undefined;
43
+ let api: ReturnType<typeof useUnmountAwareTimeout> | undefined;
44
44
 
45
45
  render(
46
46
  <TestComponent
@@ -63,7 +63,7 @@ describe("useUnmountAwareCallbacks", () => {
63
63
  it("executes multiple callbacks after their respective delays", () => {
64
64
  const callback1 = jest.fn();
65
65
  const callback2 = jest.fn();
66
- let api: ReturnType<typeof useUnmountAwareCallbacks> | undefined;
66
+ let api: ReturnType<typeof useUnmountAwareTimeout> | undefined;
67
67
 
68
68
  render(
69
69
  <TestComponent
@@ -94,7 +94,7 @@ describe("useUnmountAwareCallbacks", () => {
94
94
 
95
95
  it("clears all timeouts when the component unmounts", () => {
96
96
  const callback = jest.fn();
97
- let api: ReturnType<typeof useUnmountAwareCallbacks> | undefined;
97
+ let api: ReturnType<typeof useUnmountAwareTimeout> | undefined;
98
98
 
99
99
  const component = render(
100
100
  <TestComponent
@@ -123,7 +123,7 @@ describe("useUnmountAwareCallbacks", () => {
123
123
 
124
124
  it("removes timeout from tracking set once it executes", () => {
125
125
  const callback = jest.fn();
126
- let api: ReturnType<typeof useUnmountAwareCallbacks> | undefined;
126
+ let api: ReturnType<typeof useUnmountAwareTimeout> | undefined;
127
127
 
128
128
  const component = render(
129
129
  <TestComponent
@@ -157,7 +157,7 @@ describe("useUnmountAwareCallbacks", () => {
157
157
  const callback1 = jest.fn();
158
158
  const callback2 = jest.fn();
159
159
  const callback3 = jest.fn();
160
- let api: ReturnType<typeof useUnmountAwareCallbacks> | undefined;
160
+ let api: ReturnType<typeof useUnmountAwareTimeout> | undefined;
161
161
 
162
162
  const component = render(
163
163
  <TestComponent
@@ -193,7 +193,7 @@ describe("useUnmountAwareCallbacks", () => {
193
193
 
194
194
  it("handles callbacks that trigger new timeouts", () => {
195
195
  const finalCallback = jest.fn();
196
- let api: ReturnType<typeof useUnmountAwareCallbacks> | undefined;
196
+ let api: ReturnType<typeof useUnmountAwareTimeout> | undefined;
197
197
 
198
198
  render(
199
199
  <TestComponent
@@ -222,7 +222,7 @@ describe("useUnmountAwareCallbacks", () => {
222
222
 
223
223
  it("handles zero delay timeouts", () => {
224
224
  const callback = jest.fn();
225
- let api: ReturnType<typeof useUnmountAwareCallbacks> | undefined;
225
+ let api: ReturnType<typeof useUnmountAwareTimeout> | undefined;
226
226
 
227
227
  render(
228
228
  <TestComponent
@@ -247,7 +247,7 @@ describe("useUnmountAwareCallbacks", () => {
247
247
  throw new Error("Test error");
248
248
  });
249
249
  const successCallback = jest.fn();
250
- let api: ReturnType<typeof useUnmountAwareCallbacks> | undefined;
250
+ let api: ReturnType<typeof useUnmountAwareTimeout> | undefined;
251
251
 
252
252
  // Suppress error log during test
253
253
  const originalConsoleError = console.error;
@@ -25,7 +25,7 @@ export function useFlatListBenchmark(
25
25
  ) {
26
26
  useEffect(() => {
27
27
  const cancellable = new Cancellable();
28
- if (flatListRef.current) {
28
+ if (flatListRef.current && flatListRef.current.props) {
29
29
  if (!(Number(flatListRef.current.props.data?.length) > 0)) {
30
30
  throw new Error("Data is empty, cannot run benchmark");
31
31
  }
@@ -71,7 +71,7 @@ async function runScrollBenchmark(
71
71
  scrollSpeedMultiplier: number
72
72
  ): Promise<void> {
73
73
  if (flatListRef.current) {
74
- const horizontal = flatListRef.current.props.horizontal;
74
+ const horizontal = Boolean(flatListRef.current.props?.horizontal);
75
75
 
76
76
  const fromX = 0;
77
77
  const fromY = 0;
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { RecyclerView } from "./recyclerview/RecyclerView";
5
5
 
6
6
  // Keep this unmodified for TS type checking
7
7
  export { default as FlashList } from "./FlashList";
8
+ export { FlashListRef } from "./FlashListRef";
8
9
  export {
9
10
  FlashListProps,
10
11
  ContentStyle,