@shopify/flash-list 2.0.0-alpha.10 → 2.0.0-alpha.11
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 +295 -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 +33 -14
- 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 +11 -7
- package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerViewManager.js +57 -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.map +1 -1
- package/dist/recyclerview/components/ScrollAnchor.js +1 -1
- package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
- package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
- package/dist/recyclerview/components/StickyHeaders.js +44 -17
- 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 +93 -71
- 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 +309 -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 +38 -23
- package/src/recyclerview/RecyclerViewContextProvider.ts +12 -6
- package/src/recyclerview/RecyclerViewManager.ts +73 -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 +9 -5
- package/src/recyclerview/components/StickyHeaders.tsx +57 -19
- package/src/recyclerview/hooks/useBoundDetection.ts +25 -18
- package/src/recyclerview/hooks/useOnLoad.ts +4 -6
- package/src/recyclerview/hooks/useRecyclerViewController.tsx +104 -125
- 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,265 @@
|
|
|
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
|
+
/**
|
|
30
|
+
* @param maxItemsInRecyclePool - Maximum number of items that can be in the recycle pool
|
|
31
|
+
*/
|
|
32
|
+
constructor(maxItemsInRecyclePool: number = Number.MAX_SAFE_INTEGER) {
|
|
33
|
+
this.maxItemsInRecyclePool = maxItemsInRecyclePool;
|
|
34
|
+
this.recycleKeyPools = new Map();
|
|
35
|
+
this.keyMap = new Map();
|
|
36
|
+
this.stableIdMap = new Map();
|
|
37
|
+
this.keyCounter = 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Synchronizes the render stack with the current state of data.
|
|
42
|
+
* This method is the core orchestrator that:
|
|
43
|
+
* 1. Recycles keys for items that are no longer valid
|
|
44
|
+
* 2. Updates existing keys for items that remain visible
|
|
45
|
+
* 3. Assigns new keys for newly visible items
|
|
46
|
+
* 4. Cleans up excess items to maintain the recycling pool size
|
|
47
|
+
*
|
|
48
|
+
* @param getStableId - Function to get a stable identifier for an item at a specific index
|
|
49
|
+
* @param getItemType - Function to get the type of an item at a specific index
|
|
50
|
+
* @param engagedIndices - Collection of indices that are currently visible or engaged
|
|
51
|
+
* @param dataLength - Total length of the data set
|
|
52
|
+
*/
|
|
53
|
+
public sync(
|
|
54
|
+
getStableId: (index: number) => string,
|
|
55
|
+
getItemType: (index: number) => string,
|
|
56
|
+
engagedIndices: ConsecutiveNumbers,
|
|
57
|
+
dataLength: number
|
|
58
|
+
) {
|
|
59
|
+
if (this.disableRecycling) {
|
|
60
|
+
this.clearRecyclePool();
|
|
61
|
+
}
|
|
62
|
+
// Recycle keys for items that are no longer valid or visible
|
|
63
|
+
this.keyMap.forEach((keyInfo, key) => {
|
|
64
|
+
const { index, stableId, itemType } = keyInfo;
|
|
65
|
+
if (index >= dataLength) {
|
|
66
|
+
this.recycleKey(key);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const newStableId = getStableId(index);
|
|
70
|
+
const newItemType = getItemType(index);
|
|
71
|
+
if (stableId !== newStableId || itemType !== newItemType) {
|
|
72
|
+
this.recycleKey(key);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!engagedIndices.includes(index)) {
|
|
76
|
+
this.recycleKey(key);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// First pass: process items that already have optimized keys
|
|
81
|
+
for (const index of engagedIndices) {
|
|
82
|
+
if (this.hasOptimizedKey(getStableId(index))) {
|
|
83
|
+
this.syncItem(index, getItemType(index), getStableId(index));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Second pass: process remaining items that need new keys
|
|
88
|
+
for (const index of engagedIndices) {
|
|
89
|
+
if (!this.hasOptimizedKey(getStableId(index))) {
|
|
90
|
+
this.syncItem(index, getItemType(index), getStableId(index));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Clean up stale items and manage the recycle pool size
|
|
95
|
+
this.cleanup(getStableId, engagedIndices, dataLength);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Checks if a stable ID already has an assigned key
|
|
100
|
+
*/
|
|
101
|
+
private hasOptimizedKey(stableId: string): boolean {
|
|
102
|
+
return this.stableIdMap.has(stableId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Cleans up stale keys and manages the recycle pool size.
|
|
107
|
+
* This ensures we don't maintain references to items that are no longer in the dataset,
|
|
108
|
+
* and limits the number of recycled items to avoid excessive memory usage.
|
|
109
|
+
*/
|
|
110
|
+
private cleanup(
|
|
111
|
+
getStableId: (index: number) => string,
|
|
112
|
+
engagedIndices: ConsecutiveNumbers,
|
|
113
|
+
dataLength: number
|
|
114
|
+
) {
|
|
115
|
+
const itemsToDelete = new Array<string>();
|
|
116
|
+
|
|
117
|
+
// Remove items that are no longer in the dataset
|
|
118
|
+
for (const [key, keyInfo] of this.keyMap.entries()) {
|
|
119
|
+
const { index, itemType, stableId } = keyInfo;
|
|
120
|
+
if (index >= dataLength || getStableId(index) !== stableId) {
|
|
121
|
+
this.deleteKeyFromRecyclePool(itemType, key);
|
|
122
|
+
this.stableIdMap.delete(stableId);
|
|
123
|
+
itemsToDelete.push(key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const key of itemsToDelete) {
|
|
128
|
+
this.keyMap.delete(key);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Limit the size of the recycle pool
|
|
132
|
+
const itemsRenderedForRecycling = this.keyMap.size - engagedIndices.length;
|
|
133
|
+
if (itemsRenderedForRecycling > this.maxItemsInRecyclePool) {
|
|
134
|
+
const deleteCount =
|
|
135
|
+
itemsRenderedForRecycling - this.maxItemsInRecyclePool;
|
|
136
|
+
let deleted = 0;
|
|
137
|
+
|
|
138
|
+
// Use a for loop so we can break early once we've deleted enough items
|
|
139
|
+
const entries = Array.from(this.keyMap.entries()).reverse();
|
|
140
|
+
for (let i = 0; i < entries.length && deleted < deleteCount; i++) {
|
|
141
|
+
const [key, keyInfo] = entries[i];
|
|
142
|
+
const { index, itemType, stableId } = keyInfo;
|
|
143
|
+
|
|
144
|
+
if (!engagedIndices.includes(index)) {
|
|
145
|
+
this.deleteKeyFromRecyclePool(itemType, key);
|
|
146
|
+
this.stableIdMap.delete(stableId);
|
|
147
|
+
this.keyMap.delete(key);
|
|
148
|
+
deleted++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Places a key back into its type-specific recycle pool for future reuse
|
|
156
|
+
*/
|
|
157
|
+
private recycleKey(key: string): void {
|
|
158
|
+
if (this.disableRecycling) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const keyInfo = this.keyMap.get(key);
|
|
162
|
+
|
|
163
|
+
if (!keyInfo) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const { itemType } = keyInfo;
|
|
168
|
+
|
|
169
|
+
// Add key back to its type's pool
|
|
170
|
+
const pool = this.getRecyclePoolForType(itemType);
|
|
171
|
+
|
|
172
|
+
pool.add(key);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns the current render stack containing all active keys and their metadata
|
|
177
|
+
*/
|
|
178
|
+
public getRenderStack() {
|
|
179
|
+
return this.keyMap;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Syncs an individual item by assigning it an appropriate key.
|
|
184
|
+
* Will use an existing key if available, or generate a new one.
|
|
185
|
+
*
|
|
186
|
+
* @returns The key assigned to the item
|
|
187
|
+
*/
|
|
188
|
+
private syncItem(index: number, itemType: string, stableId: string): string {
|
|
189
|
+
// Try to reuse an existing key, or get one from the recycle pool, or generate a new one
|
|
190
|
+
const newKey =
|
|
191
|
+
this.stableIdMap.get(stableId) ||
|
|
192
|
+
this.getKeyFromRecyclePool(itemType) ||
|
|
193
|
+
this.generateKey();
|
|
194
|
+
|
|
195
|
+
const keyInfo = this.keyMap.get(newKey);
|
|
196
|
+
if (keyInfo) {
|
|
197
|
+
// Update an existing key's metadata
|
|
198
|
+
this.deleteKeyFromRecyclePool(itemType, newKey);
|
|
199
|
+
this.deleteKeyFromRecyclePool(keyInfo.itemType, newKey);
|
|
200
|
+
this.stableIdMap.delete(keyInfo.stableId);
|
|
201
|
+
keyInfo.index = index;
|
|
202
|
+
keyInfo.itemType = itemType;
|
|
203
|
+
keyInfo.stableId = stableId;
|
|
204
|
+
} else {
|
|
205
|
+
// Create a new entry in the key map
|
|
206
|
+
this.keyMap.set(newKey, {
|
|
207
|
+
itemType,
|
|
208
|
+
index,
|
|
209
|
+
stableId,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
this.stableIdMap.set(stableId, newKey);
|
|
213
|
+
|
|
214
|
+
return newKey;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Clears all recycled keys from the pool, effectively resetting the recycling system.
|
|
219
|
+
* This operation does not affect currently active keys.
|
|
220
|
+
*/
|
|
221
|
+
private clearRecyclePool() {
|
|
222
|
+
this.recycleKeyPools.clear();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Generates a unique sequential key using an internal counter.
|
|
227
|
+
* @returns A unique key as a string
|
|
228
|
+
*/
|
|
229
|
+
private generateKey(): string {
|
|
230
|
+
return (this.keyCounter++).toString();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Removes a specific key from its type's recycle pool
|
|
235
|
+
*/
|
|
236
|
+
private deleteKeyFromRecyclePool(itemType: string, key: string) {
|
|
237
|
+
this.recycleKeyPools.get(itemType)?.delete(key);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Gets or creates a recycle pool for a specific item type
|
|
242
|
+
*/
|
|
243
|
+
private getRecyclePoolForType(itemType: string) {
|
|
244
|
+
let pool = this.recycleKeyPools.get(itemType);
|
|
245
|
+
if (!pool) {
|
|
246
|
+
pool = new Set();
|
|
247
|
+
this.recycleKeyPools.set(itemType, pool);
|
|
248
|
+
}
|
|
249
|
+
return pool;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Retrieves and removes a key from the type's recycle pool
|
|
254
|
+
* @returns A recycled key or undefined if none available
|
|
255
|
+
*/
|
|
256
|
+
private getKeyFromRecyclePool(itemType: string) {
|
|
257
|
+
const pool = this.getRecyclePoolForType(itemType);
|
|
258
|
+
if (pool.size > 0) {
|
|
259
|
+
const key = pool.values().next().value;
|
|
260
|
+
pool.delete(key);
|
|
261
|
+
return key;
|
|
262
|
+
}
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -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
|
-
|
|
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<
|
|
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 */
|
|
@@ -99,27 +99,37 @@ export const ViewHolderCollection = <TItem,>(
|
|
|
99
99
|
// );
|
|
100
100
|
recyclerViewContext?.layout();
|
|
101
101
|
}
|
|
102
|
+
// we need to run this callback on when fixedContainerSize changes
|
|
103
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
102
104
|
}, [fixedContainerSize]);
|
|
103
105
|
|
|
104
106
|
useLayoutEffect(() => {
|
|
105
107
|
if (renderId > 0) {
|
|
106
108
|
onCommitLayoutEffect?.();
|
|
107
109
|
}
|
|
110
|
+
// we need to run this callback on when renderId changes
|
|
111
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
108
112
|
}, [renderId]);
|
|
109
113
|
|
|
110
114
|
useEffect(() => {
|
|
111
115
|
if (renderId > 0) {
|
|
112
116
|
onCommitEffect?.();
|
|
113
117
|
}
|
|
118
|
+
// we need to run this callback on when renderId changes
|
|
119
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
114
120
|
}, [renderId]);
|
|
115
121
|
|
|
116
122
|
// Expose forceUpdate through ref
|
|
117
|
-
useImperativeHandle(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
useImperativeHandle(
|
|
124
|
+
viewHolderCollectionRef,
|
|
125
|
+
() => ({
|
|
126
|
+
commitLayout: () => {
|
|
127
|
+
// This will trigger a re-render of the component
|
|
128
|
+
setRenderId((prev) => prev + 1);
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
[setRenderId]
|
|
132
|
+
);
|
|
123
133
|
|
|
124
134
|
const hasData = data && data.length > 0;
|
|
125
135
|
|
|
@@ -128,6 +138,17 @@ export const ViewHolderCollection = <TItem,>(
|
|
|
128
138
|
height: containerLayout?.height,
|
|
129
139
|
};
|
|
130
140
|
|
|
141
|
+
// sort by index and log
|
|
142
|
+
// const sortedRenderStack = Array.from(renderStack.entries()).sort(
|
|
143
|
+
// ([, a], [, b]) => a.index - b.index
|
|
144
|
+
// );
|
|
145
|
+
// console.log(
|
|
146
|
+
// "sortedRenderStack",
|
|
147
|
+
// sortedRenderStack.map(([reactKey, { index }]) => {
|
|
148
|
+
// return `${index} => ${reactKey}`;
|
|
149
|
+
// })
|
|
150
|
+
// );
|
|
151
|
+
|
|
131
152
|
return (
|
|
132
153
|
<CompatView
|
|
133
154
|
// TODO: Take care of web scroll bar here
|
|
@@ -135,7 +156,7 @@ export const ViewHolderCollection = <TItem,>(
|
|
|
135
156
|
>
|
|
136
157
|
{containerLayout &&
|
|
137
158
|
hasData &&
|
|
138
|
-
Array.from(renderStack, ([
|
|
159
|
+
Array.from(renderStack.entries(), ([reactKey, { index }]) => {
|
|
139
160
|
const item = data[index];
|
|
140
161
|
const trailingItem = ItemSeparatorComponent
|
|
141
162
|
? data[index + 1]
|
|
@@ -35,11 +35,15 @@ export function ScrollAnchor({ scrollAnchorRef }: ScrollAnchorProps) {
|
|
|
35
35
|
const [scrollOffset, setScrollOffset] = useState(1000000); // TODO: Fix this value
|
|
36
36
|
|
|
37
37
|
// Expose scrollBy method through ref
|
|
38
|
-
useImperativeHandle(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
useImperativeHandle(
|
|
39
|
+
scrollAnchorRef,
|
|
40
|
+
() => ({
|
|
41
|
+
scrollBy: (offset: number) => {
|
|
42
|
+
setScrollOffset((prev) => prev + offset);
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
[]
|
|
46
|
+
);
|
|
43
47
|
|
|
44
48
|
// Create an invisible anchor element that can be positioned
|
|
45
49
|
const anchor = useMemo(() => {
|
|
@@ -58,25 +58,45 @@ export const StickyHeaders = <TItem,>({
|
|
|
58
58
|
data,
|
|
59
59
|
extraData,
|
|
60
60
|
}: StickyHeaderProps<TItem>) => {
|
|
61
|
-
const [
|
|
61
|
+
const [stickyHeaderState, setStickyHeaderState] = useState<{
|
|
62
62
|
currentStickyIndex: number;
|
|
63
63
|
nextStickyIndex: number;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
currentStickyHeight: number;
|
|
65
|
+
nextStickyY: number;
|
|
66
|
+
}>({
|
|
67
|
+
currentStickyIndex: -1,
|
|
68
|
+
nextStickyIndex: -1,
|
|
69
|
+
currentStickyHeight: 0,
|
|
70
|
+
nextStickyY: 0,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const {
|
|
74
|
+
currentStickyIndex,
|
|
75
|
+
nextStickyIndex,
|
|
76
|
+
nextStickyY,
|
|
77
|
+
currentStickyHeight,
|
|
78
|
+
} = stickyHeaderState;
|
|
67
79
|
|
|
68
80
|
// Memoize sorted indices based on their Y positions
|
|
69
81
|
const sortedIndices = useMemo(() => {
|
|
70
82
|
return stickyHeaderIndices.sort((first, second) => first - second);
|
|
71
83
|
}, [stickyHeaderIndices]);
|
|
72
84
|
|
|
85
|
+
const legthInvalid =
|
|
86
|
+
sortedIndices.length === 0 ||
|
|
87
|
+
recyclerViewManager.getDataLength() <=
|
|
88
|
+
sortedIndices[sortedIndices.length - 1];
|
|
89
|
+
|
|
73
90
|
const compute = useCallback(() => {
|
|
74
|
-
|
|
91
|
+
if (legthInvalid) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const adjustedScrollOffset = recyclerViewManager.getLastScrollOffset();
|
|
75
95
|
|
|
76
96
|
// Binary search for current sticky index
|
|
77
97
|
const currentIndexInArray = findCurrentStickyIndex(
|
|
78
98
|
sortedIndices,
|
|
79
|
-
|
|
99
|
+
adjustedScrollOffset,
|
|
80
100
|
(index) => recyclerViewManager.getLayout(index).y
|
|
81
101
|
);
|
|
82
102
|
|
|
@@ -87,16 +107,34 @@ export const StickyHeaders = <TItem,>({
|
|
|
87
107
|
newNextStickyIndex = -1;
|
|
88
108
|
}
|
|
89
109
|
|
|
110
|
+
const newNextStickyY =
|
|
111
|
+
(recyclerViewManager.tryGetLayout(newNextStickyIndex)?.y ?? 0) +
|
|
112
|
+
recyclerViewManager.firstItemOffset;
|
|
113
|
+
const newCurrentStickyHeight =
|
|
114
|
+
recyclerViewManager.tryGetLayout(newStickyIndex)?.height ?? 0;
|
|
115
|
+
|
|
90
116
|
if (
|
|
91
117
|
newStickyIndex !== currentStickyIndex ||
|
|
92
|
-
newNextStickyIndex !== nextStickyIndex
|
|
118
|
+
newNextStickyIndex !== nextStickyIndex ||
|
|
119
|
+
newNextStickyY !== nextStickyY ||
|
|
120
|
+
newCurrentStickyHeight !== currentStickyHeight
|
|
93
121
|
) {
|
|
94
|
-
|
|
122
|
+
setStickyHeaderState({
|
|
95
123
|
currentStickyIndex: newStickyIndex,
|
|
96
124
|
nextStickyIndex: newNextStickyIndex,
|
|
125
|
+
nextStickyY: newNextStickyY,
|
|
126
|
+
currentStickyHeight: newCurrentStickyHeight,
|
|
97
127
|
});
|
|
98
128
|
}
|
|
99
|
-
}, [
|
|
129
|
+
}, [
|
|
130
|
+
currentStickyIndex,
|
|
131
|
+
nextStickyIndex,
|
|
132
|
+
recyclerViewManager,
|
|
133
|
+
sortedIndices,
|
|
134
|
+
legthInvalid,
|
|
135
|
+
nextStickyY,
|
|
136
|
+
currentStickyHeight,
|
|
137
|
+
]);
|
|
100
138
|
|
|
101
139
|
useEffect(() => {
|
|
102
140
|
compute();
|
|
@@ -125,20 +163,20 @@ export const StickyHeaders = <TItem,>({
|
|
|
125
163
|
});
|
|
126
164
|
}
|
|
127
165
|
|
|
128
|
-
const
|
|
129
|
-
const nextLayout = recyclerViewManager.getLayout(nextStickyIndex);
|
|
130
|
-
|
|
131
|
-
const pushStartsAt = nextLayout.y - currentLayout.height;
|
|
166
|
+
const pushStartsAt = nextStickyY - currentStickyHeight;
|
|
132
167
|
|
|
133
168
|
return scrollY.interpolate({
|
|
134
|
-
inputRange: [
|
|
135
|
-
|
|
136
|
-
nextLayout.y + recyclerViewManager.firstItemOffset,
|
|
137
|
-
],
|
|
138
|
-
outputRange: [0, -currentLayout.height],
|
|
169
|
+
inputRange: [pushStartsAt, nextStickyY],
|
|
170
|
+
outputRange: [0, -currentStickyHeight],
|
|
139
171
|
extrapolate: "clamp",
|
|
140
172
|
});
|
|
141
|
-
}, [
|
|
173
|
+
}, [
|
|
174
|
+
currentStickyHeight,
|
|
175
|
+
currentStickyIndex,
|
|
176
|
+
nextStickyIndex,
|
|
177
|
+
nextStickyY,
|
|
178
|
+
scrollY,
|
|
179
|
+
]);
|
|
142
180
|
|
|
143
181
|
// Memoize header content
|
|
144
182
|
const headerContent = useMemo(() => {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
import { RecyclerViewManager } from "../RecyclerViewManager";
|
|
4
|
-
import { RecyclerViewProps } from "../RecyclerViewProps";
|
|
5
4
|
import { CompatScroller } from "../components/CompatScroller";
|
|
6
5
|
|
|
6
|
+
import { useUnmountAwareAnimationFrame } from "./useUnmountAwareCallbacks";
|
|
7
|
+
|
|
7
8
|
/**
|
|
8
9
|
* Hook to detect when the scroll position reaches near the start or end of the list
|
|
9
10
|
* and trigger the appropriate callbacks. This hook is responsible for:
|
|
@@ -17,7 +18,6 @@ import { CompatScroller } from "../components/CompatScroller";
|
|
|
17
18
|
*/
|
|
18
19
|
export function useBoundDetection<T>(
|
|
19
20
|
recyclerViewManager: RecyclerViewManager<T>,
|
|
20
|
-
props: RecyclerViewProps<T>,
|
|
21
21
|
scrollViewRef: React.RefObject<CompatScroller>
|
|
22
22
|
) {
|
|
23
23
|
// Track whether we've already triggered the end reached callback to prevent duplicate calls
|
|
@@ -26,22 +26,27 @@ export function useBoundDetection<T>(
|
|
|
26
26
|
const pendingStartReached = useRef(false);
|
|
27
27
|
// Track whether we should auto-scroll to bottom when new content is added
|
|
28
28
|
const pendingAutoscrollToBottom = useRef(false);
|
|
29
|
-
const {
|
|
29
|
+
const { data } = recyclerViewManager.props;
|
|
30
|
+
const { requestAnimationFrame } = useUnmountAwareAnimationFrame();
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Checks if the scroll position is near the start or end of the list
|
|
33
34
|
* and triggers appropriate callbacks if configured.
|
|
34
35
|
*/
|
|
35
36
|
const checkBounds = useCallback(() => {
|
|
37
|
+
const {
|
|
38
|
+
onEndReached,
|
|
39
|
+
onStartReached,
|
|
40
|
+
maintainVisibleContentPosition,
|
|
41
|
+
horizontal,
|
|
42
|
+
onEndReachedThreshold: onEndReachedThresholdProp,
|
|
43
|
+
onStartReachedThreshold: onStartReachedThresholdProp,
|
|
44
|
+
} = recyclerViewManager.props;
|
|
36
45
|
// Skip all calculations if neither callback is provided and autoscroll is disabled
|
|
37
46
|
const autoscrollToBottomThreshold =
|
|
38
47
|
maintainVisibleContentPosition?.autoscrollToBottomThreshold ?? -1;
|
|
39
48
|
|
|
40
|
-
if (
|
|
41
|
-
!props.onEndReached &&
|
|
42
|
-
!props.onStartReached &&
|
|
43
|
-
autoscrollToBottomThreshold < 0
|
|
44
|
-
) {
|
|
49
|
+
if (!onEndReached && !onStartReached && autoscrollToBottomThreshold < 0) {
|
|
45
50
|
return;
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -50,7 +55,7 @@ export function useBoundDetection<T>(
|
|
|
50
55
|
recyclerViewManager.getAbsoluteLastScrollOffset();
|
|
51
56
|
const contentSize = recyclerViewManager.getChildContainerDimensions();
|
|
52
57
|
const windowSize = recyclerViewManager.getWindowSize();
|
|
53
|
-
const isHorizontal =
|
|
58
|
+
const isHorizontal = horizontal === true;
|
|
54
59
|
|
|
55
60
|
// Calculate dimensions based on scroll direction
|
|
56
61
|
const visibleLength = isHorizontal ? windowSize.width : windowSize.height;
|
|
@@ -59,8 +64,8 @@ export function useBoundDetection<T>(
|
|
|
59
64
|
recyclerViewManager.firstItemOffset;
|
|
60
65
|
|
|
61
66
|
// Check if we're near the end of the list
|
|
62
|
-
if (
|
|
63
|
-
const onEndReachedThreshold =
|
|
67
|
+
if (onEndReached) {
|
|
68
|
+
const onEndReachedThreshold = onEndReachedThresholdProp ?? 0.5;
|
|
64
69
|
const endThresholdDistance = onEndReachedThreshold * visibleLength;
|
|
65
70
|
|
|
66
71
|
const isNearEnd =
|
|
@@ -69,27 +74,27 @@ export function useBoundDetection<T>(
|
|
|
69
74
|
|
|
70
75
|
if (isNearEnd && !pendingEndReached.current) {
|
|
71
76
|
pendingEndReached.current = true;
|
|
72
|
-
|
|
77
|
+
onEndReached();
|
|
73
78
|
}
|
|
74
79
|
pendingEndReached.current = isNearEnd;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
// Check if we're near the start of the list
|
|
78
|
-
if (
|
|
79
|
-
const onStartReachedThreshold =
|
|
83
|
+
if (onStartReached) {
|
|
84
|
+
const onStartReachedThreshold = onStartReachedThresholdProp ?? 0.2;
|
|
80
85
|
const startThresholdDistance = onStartReachedThreshold * visibleLength;
|
|
81
86
|
|
|
82
87
|
const isNearStart = lastScrollOffset <= startThresholdDistance;
|
|
83
88
|
|
|
84
89
|
if (isNearStart && !pendingStartReached.current) {
|
|
85
90
|
pendingStartReached.current = true;
|
|
86
|
-
|
|
91
|
+
onStartReached();
|
|
87
92
|
}
|
|
88
93
|
pendingStartReached.current = isNearStart;
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
// Handle auto-scrolling to bottom for vertical lists
|
|
92
|
-
if (!
|
|
97
|
+
if (!isHorizontal) {
|
|
93
98
|
const autoscrollToBottomThresholdDistance =
|
|
94
99
|
autoscrollToBottomThreshold * visibleLength;
|
|
95
100
|
|
|
@@ -104,11 +109,13 @@ export function useBoundDetection<T>(
|
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
|
-
}, [recyclerViewManager
|
|
112
|
+
}, [recyclerViewManager]);
|
|
108
113
|
|
|
109
114
|
// Reset end reached state when data changes
|
|
110
115
|
useMemo(() => {
|
|
111
116
|
pendingEndReached.current = false;
|
|
117
|
+
// needs to run only when data changes
|
|
118
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
112
119
|
}, [data]);
|
|
113
120
|
|
|
114
121
|
// Auto-scroll to bottom when new content is added and we're near the bottom
|
|
@@ -119,7 +126,7 @@ export function useBoundDetection<T>(
|
|
|
119
126
|
pendingAutoscrollToBottom.current = false;
|
|
120
127
|
});
|
|
121
128
|
}
|
|
122
|
-
}, [data]);
|
|
129
|
+
}, [data, requestAnimationFrame, scrollViewRef]);
|
|
123
130
|
|
|
124
131
|
return {
|
|
125
132
|
checkBounds,
|
|
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
|
|
3
3
|
import { RecyclerViewManager } from "../RecyclerViewManager";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { useUnmountAwareAnimationFrame } from "./useUnmountAwareCallbacks";
|
|
6
6
|
// import { ToastAndroid } from "react-native";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -22,7 +22,7 @@ export const useOnListLoad = <T>(
|
|
|
22
22
|
const [isLoaded, setIsLoaded] = useState<boolean>(false);
|
|
23
23
|
const dataLength = recyclerViewManager.getDataLength();
|
|
24
24
|
// const dataCollector = useRef<number[]>([]);
|
|
25
|
-
const
|
|
25
|
+
const { requestAnimationFrame } = useUnmountAwareAnimationFrame();
|
|
26
26
|
// Track render cycles by collecting elapsed time on each render
|
|
27
27
|
// useEffect(() => {
|
|
28
28
|
// const elapsedTimeInMs = Date.now() - loadStartTimeRef.current;
|
|
@@ -48,10 +48,8 @@ export const useOnListLoad = <T>(
|
|
|
48
48
|
// console.log("----------> dataCollector", dataCollectorString);
|
|
49
49
|
// console.log("----------> FlashList v2 load in", `${elapsedTimeInMs} ms`);
|
|
50
50
|
requestAnimationFrame(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
setIsLoaded(true);
|
|
54
|
-
}
|
|
51
|
+
onLoad?.({ elapsedTimeInMs });
|
|
52
|
+
setIsLoaded(true);
|
|
55
53
|
});
|
|
56
54
|
});
|
|
57
55
|
|