@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.
- package/README.md +6 -2
- package/dist/AnimatedFlashList.d.ts.map +1 -1
- package/dist/AnimatedFlashList.js +3 -3
- package/dist/AnimatedFlashList.js.map +1 -1
- package/dist/FlashList.d.ts +9 -0
- package/dist/FlashList.d.ts.map +1 -1
- package/dist/FlashList.js +20 -0
- package/dist/FlashList.js.map +1 -1
- package/dist/FlashListProps.d.ts +13 -6
- package/dist/FlashListProps.d.ts.map +1 -1
- package/dist/FlashListProps.js.map +1 -1
- package/dist/FlashListRef.d.ts +305 -0
- package/dist/FlashListRef.d.ts.map +1 -0
- package/dist/FlashListRef.js +3 -0
- package/dist/FlashListRef.js.map +1 -0
- package/dist/__tests__/RecyclerView.test.js +62 -27
- package/dist/__tests__/RecyclerView.test.js.map +1 -1
- package/dist/__tests__/RenderStackManager.test.d.ts +2 -0
- package/dist/__tests__/RenderStackManager.test.d.ts.map +1 -0
- package/dist/__tests__/RenderStackManager.test.js +405 -0
- package/dist/__tests__/RenderStackManager.test.js.map +1 -0
- package/dist/__tests__/useUnmountAwareCallbacks.test.js +1 -1
- package/dist/__tests__/useUnmountAwareCallbacks.test.js.map +1 -1
- package/dist/benchmark/useFlatListBenchmark.js +8 -7
- package/dist/benchmark/useFlatListBenchmark.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/recyclerview/RecyclerView.d.ts +2 -1
- package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerView.js +39 -21
- package/dist/recyclerview/RecyclerView.js.map +1 -1
- package/dist/recyclerview/RecyclerViewContextProvider.d.ts +6 -5
- package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -1
- package/dist/recyclerview/RecyclerViewManager.d.ts +14 -7
- package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerViewManager.js +71 -102
- package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
- package/dist/recyclerview/RenderStackManager.d.ts +85 -0
- package/dist/recyclerview/RenderStackManager.d.ts.map +1 -0
- package/dist/recyclerview/RenderStackManager.js +261 -0
- package/dist/recyclerview/RenderStackManager.js.map +1 -0
- package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
- package/dist/recyclerview/ViewHolder.js +5 -3
- package/dist/recyclerview/ViewHolder.js.map +1 -1
- package/dist/recyclerview/ViewHolderCollection.d.ts +3 -1
- package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
- package/dist/recyclerview/ViewHolderCollection.js +19 -3
- package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
- package/dist/recyclerview/components/ScrollAnchor.d.ts +2 -1
- package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -1
- package/dist/recyclerview/components/ScrollAnchor.js +9 -4
- package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
- package/dist/recyclerview/components/StickyHeaders.d.ts +1 -1
- package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
- package/dist/recyclerview/components/StickyHeaders.js +39 -32
- package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
- package/dist/recyclerview/hooks/useBoundDetection.d.ts +1 -2
- package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useBoundDetection.js +19 -16
- package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
- package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useOnLoad.js +4 -6
- package/dist/recyclerview/hooks/useOnLoad.js.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +3 -48
- package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewController.js +111 -77
- package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewManager.js +6 -0
- package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
- package/dist/recyclerview/hooks/useSecondaryProps.js +1 -1
- package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts +10 -3
- package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js +33 -4
- package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js.map +1 -1
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +6 -0
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/GridLayoutManager.js +27 -5
- package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
- package/dist/recyclerview/layout-managers/LayoutManager.d.ts +2 -2
- package/dist/recyclerview/layout-managers/LayoutManager.js +2 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/jestSetup.js +30 -11
- package/package.json +1 -1
- package/src/AnimatedFlashList.ts +3 -2
- package/src/FlashList.tsx +24 -0
- package/src/FlashListProps.ts +16 -7
- package/src/FlashListRef.ts +320 -0
- package/src/__tests__/RecyclerView.test.tsx +83 -29
- package/src/__tests__/RenderStackManager.test.ts +488 -0
- package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
- package/src/benchmark/useFlatListBenchmark.ts +2 -2
- package/src/index.ts +1 -0
- package/src/recyclerview/RecyclerView.tsx +49 -29
- package/src/recyclerview/RecyclerViewContextProvider.ts +12 -6
- package/src/recyclerview/RecyclerViewManager.ts +90 -88
- package/src/recyclerview/RenderStackManager.ts +265 -0
- package/src/recyclerview/ViewHolder.tsx +5 -3
- package/src/recyclerview/ViewHolderCollection.tsx +29 -8
- package/src/recyclerview/components/ScrollAnchor.tsx +21 -8
- package/src/recyclerview/components/StickyHeaders.tsx +62 -44
- package/src/recyclerview/hooks/useBoundDetection.ts +25 -18
- package/src/recyclerview/hooks/useOnLoad.ts +4 -6
- package/src/recyclerview/hooks/useRecyclerViewController.tsx +121 -132
- package/src/recyclerview/hooks/useRecyclerViewManager.ts +6 -0
- package/src/recyclerview/hooks/useSecondaryProps.tsx +1 -1
- package/src/recyclerview/hooks/useUnmountAwareCallbacks.ts +39 -3
- package/src/recyclerview/layout-managers/GridLayoutManager.ts +30 -7
- package/src/recyclerview/layout-managers/LayoutManager.ts +2 -2
- package/dist/__tests__/RecycleKeyManager.test.d.ts +0 -2
- package/dist/__tests__/RecycleKeyManager.test.d.ts.map +0 -1
- package/dist/__tests__/RecycleKeyManager.test.js +0 -210
- package/dist/__tests__/RecycleKeyManager.test.js.map +0 -1
- package/dist/recyclerview/RecycleKeyManager.d.ts +0 -82
- package/dist/recyclerview/RecycleKeyManager.d.ts.map +0 -1
- package/dist/recyclerview/RecycleKeyManager.js +0 -135
- package/dist/recyclerview/RecycleKeyManager.js.map +0 -1
- package/src/__tests__/RecycleKeyManager.test.ts +0 -254
- 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 {
|
|
4
|
+
import { useUnmountAwareTimeout } from "../recyclerview/hooks/useUnmountAwareCallbacks";
|
|
5
5
|
|
|
6
6
|
const TestComponent = ({
|
|
7
7
|
onRender,
|
|
8
8
|
}: {
|
|
9
|
-
onRender: (api: ReturnType<typeof
|
|
9
|
+
onRender: (api: ReturnType<typeof useUnmountAwareTimeout>) => void;
|
|
10
10
|
}) => {
|
|
11
|
-
const api =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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