@shopify/flash-list 2.0.0-rc.1 → 2.0.0-rc.3

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 (70) hide show
  1. package/README.md +1 -1
  2. package/dist/FlashListProps.d.ts +10 -2
  3. package/dist/FlashListProps.d.ts.map +1 -1
  4. package/dist/FlashListProps.js.map +1 -1
  5. package/dist/__tests__/RenderStackManager.test.js +1 -2
  6. package/dist/__tests__/RenderStackManager.test.js.map +1 -1
  7. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  8. package/dist/recyclerview/RecyclerView.js +11 -3
  9. package/dist/recyclerview/RecyclerView.js.map +1 -1
  10. package/dist/recyclerview/RecyclerViewManager.d.ts +1 -0
  11. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
  12. package/dist/recyclerview/RecyclerViewManager.js +5 -0
  13. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  14. package/dist/recyclerview/RenderStackManager.d.ts +1 -0
  15. package/dist/recyclerview/RenderStackManager.d.ts.map +1 -1
  16. package/dist/recyclerview/RenderStackManager.js +26 -7
  17. package/dist/recyclerview/RenderStackManager.js.map +1 -1
  18. package/dist/recyclerview/components/StickyHeaders.js +1 -1
  19. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  20. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts +1 -0
  21. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts.map +1 -1
  22. package/dist/recyclerview/helpers/RenderTimeTracker.js +3 -0
  23. package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -1
  24. package/dist/recyclerview/hooks/useLayoutState.d.ts +3 -1
  25. package/dist/recyclerview/hooks/useLayoutState.d.ts.map +1 -1
  26. package/dist/recyclerview/hooks/useLayoutState.js +5 -3
  27. package/dist/recyclerview/hooks/useLayoutState.js.map +1 -1
  28. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +2 -1
  29. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  30. package/dist/recyclerview/hooks/useRecyclerViewController.js +232 -187
  31. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  32. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
  33. package/dist/recyclerview/hooks/useRecyclerViewManager.js +2 -1
  34. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
  35. package/dist/recyclerview/hooks/useRecyclingState.d.ts +4 -2
  36. package/dist/recyclerview/hooks/useRecyclingState.d.ts.map +1 -1
  37. package/dist/recyclerview/hooks/useRecyclingState.js +2 -2
  38. package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -1
  39. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +14 -6
  40. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  41. package/dist/recyclerview/layout-managers/GridLayoutManager.js +40 -23
  42. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  43. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +26 -6
  44. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
  45. package/dist/recyclerview/layout-managers/LayoutManager.js +89 -15
  46. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
  47. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts +9 -1
  48. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -1
  49. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +28 -12
  50. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
  51. package/dist/tsconfig.tsbuildinfo +1 -1
  52. package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
  53. package/dist/viewability/ViewabilityManager.js +10 -3
  54. package/dist/viewability/ViewabilityManager.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/FlashListProps.ts +16 -2
  57. package/src/__tests__/RenderStackManager.test.ts +1 -2
  58. package/src/recyclerview/RecyclerView.tsx +27 -14
  59. package/src/recyclerview/RecyclerViewManager.ts +6 -0
  60. package/src/recyclerview/RenderStackManager.ts +32 -6
  61. package/src/recyclerview/components/StickyHeaders.tsx +1 -1
  62. package/src/recyclerview/helpers/RenderTimeTracker.ts +4 -0
  63. package/src/recyclerview/hooks/useLayoutState.ts +15 -6
  64. package/src/recyclerview/hooks/useRecyclerViewController.tsx +232 -165
  65. package/src/recyclerview/hooks/useRecyclerViewManager.ts +3 -1
  66. package/src/recyclerview/hooks/useRecyclingState.ts +11 -7
  67. package/src/recyclerview/layout-managers/GridLayoutManager.ts +44 -23
  68. package/src/recyclerview/layout-managers/LayoutManager.ts +98 -20
  69. package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +30 -8
  70. package/src/viewability/ViewabilityManager.ts +10 -6
@@ -57,43 +57,71 @@ export function useRecyclerViewController<T>(
57
57
  // Track the first visible item for maintaining scroll position
58
58
  const firstVisibleItemKey = useRef<string | undefined>(undefined);
59
59
  const firstVisibleItemLayout = useRef<RVLayout | undefined>(undefined);
60
- const pendingScrollResolves = useRef<(() => void)[]>([]);
60
+
61
+ // Queue to store callbacks that should be executed after scroll offset updates
62
+ const pendingScrollCallbacks = useRef<(() => void)[]>([]);
61
63
 
62
64
  // Handle initial scroll position when the list first loads
63
65
  // useOnLoad(recyclerViewManager, () => {
64
66
 
65
67
  // });
66
68
  /**
67
- * Updates the scroll offset and returns a Promise that resolves
68
- * when the update has been applied.
69
+ * Updates the scroll offset and calls the provided callback
70
+ * after the update has been applied and the component has re-rendered.
71
+ *
72
+ * @param offset - The new scroll offset to apply
73
+ * @param callback - Optional callback to execute after the update is applied
69
74
  */
70
- const updateScrollOffsetAsync = useCallback(
71
- async (offset: number): Promise<void> => {
72
- return new Promise((resolve) => {
73
- // TODO: Make sure we don't scroll beyond content size
74
- if (recyclerViewManager.updateScrollOffset(offset) !== undefined) {
75
- // Add the resolve function to the queue
76
- pendingScrollResolves.current.push(resolve);
77
- setRenderId((prev) => prev + 1);
78
- } else {
79
- resolve();
80
- }
81
- });
75
+ const updateScrollOffsetWithCallback = useCallback(
76
+ (offset: number, callback: () => void): void => {
77
+ // Attempt to update the scroll offset in the RecyclerViewManager
78
+ // This returns undefined if no update is needed
79
+ if (recyclerViewManager.updateScrollOffset(offset) !== undefined) {
80
+ // It will be executed after the next render
81
+ pendingScrollCallbacks.current.push(callback);
82
+ // Trigger a re-render to apply the scroll offset update
83
+ setRenderId((prev) => prev + 1);
84
+ } else {
85
+ // No update needed, execute callback immediately
86
+ callback();
87
+ }
82
88
  },
83
89
  [recyclerViewManager]
84
90
  );
85
91
 
92
+ const computeFirstVisibleIndexForOffsetCorrection = useCallback(() => {
93
+ const { data, keyExtractor } = recyclerViewManager.props;
94
+ if (data && keyExtractor) {
95
+ // Update the tracked first visible item
96
+ const firstVisibleIndex = Math.max(
97
+ 0,
98
+ recyclerViewManager.computeVisibleIndices().startIndex
99
+ );
100
+ if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
101
+ firstVisibleItemKey.current = keyExtractor(
102
+ data![firstVisibleIndex],
103
+ firstVisibleIndex
104
+ );
105
+ firstVisibleItemLayout.current = {
106
+ ...recyclerViewManager.getLayout(firstVisibleIndex),
107
+ };
108
+ }
109
+ }
110
+ }, [recyclerViewManager]);
111
+
86
112
  /**
87
113
  * Maintains the visible content position when the list updates.
88
114
  * This is particularly useful for chat applications where we want to keep
89
115
  * the user's current view position when new messages are added.
90
116
  */
91
- const applyContentOffset = useCallback(async () => {
117
+ const applyOffsetCorrection = useCallback(() => {
92
118
  const { horizontal, data, keyExtractor } = recyclerViewManager.props;
93
- // Resolve all pending scroll updates from previous calls
94
- const resolves = pendingScrollResolves.current;
95
- pendingScrollResolves.current = [];
96
- resolves.forEach((resolve) => resolve());
119
+
120
+ // Execute all pending callbacks from previous scroll offset updates
121
+ // This ensures any scroll operations that were waiting for render are completed
122
+ const callbacks = pendingScrollCallbacks.current;
123
+ pendingScrollCallbacks.current = [];
124
+ callbacks.forEach((callback) => callback());
97
125
 
98
126
  const currentDataLength = data?.length ?? 0;
99
127
 
@@ -152,8 +180,9 @@ export function useRecyclerViewController<T>(
152
180
  scrollViewRef.current?.scrollTo(scrollToParams);
153
181
  }
154
182
  if (hasDataChanged) {
155
- updateScrollOffsetAsync(
156
- recyclerViewManager.getAbsoluteLastScrollOffset() + diff
183
+ updateScrollOffsetWithCallback(
184
+ recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
185
+ () => {}
157
186
  );
158
187
  recyclerViewManager.ignoreScrollEvents = true;
159
188
  setTimeout(() => {
@@ -164,20 +193,7 @@ export function useRecyclerViewController<T>(
164
193
  }
165
194
  }
166
195
 
167
- // Update the tracked first visible item
168
- const firstVisibleIndex = Math.max(
169
- 0,
170
- recyclerViewManager.computeVisibleIndices().startIndex
171
- );
172
- if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
173
- firstVisibleItemKey.current = keyExtractor(
174
- data![firstVisibleIndex],
175
- firstVisibleIndex
176
- );
177
- firstVisibleItemLayout.current = {
178
- ...recyclerViewManager.getLayout(firstVisibleIndex),
179
- };
180
- }
196
+ computeFirstVisibleIndexForOffsetCorrection();
181
197
  }
182
198
  lastDataLengthRef.current = data?.length ?? 0;
183
199
  }, [
@@ -185,7 +201,8 @@ export function useRecyclerViewController<T>(
185
201
  scrollAnchorRef,
186
202
  scrollViewRef,
187
203
  setTimeout,
188
- updateScrollOffsetAsync,
204
+ updateScrollOffsetWithCallback,
205
+ computeFirstVisibleIndexForOffsetCorrection,
189
206
  ]);
190
207
 
191
208
  const handlerMethods: FlashListRef<T> = useMemo(() => {
@@ -278,144 +295,189 @@ export function useRecyclerViewController<T>(
278
295
  /**
279
296
  * Scrolls to a specific index in the list.
280
297
  * Supports viewPosition and viewOffset for precise positioning.
298
+ * Returns a Promise that resolves when the scroll is complete.
281
299
  */
282
- scrollToIndex: async ({
300
+ scrollToIndex: ({
283
301
  index,
284
302
  animated,
285
303
  viewPosition,
286
304
  viewOffset,
287
- }: ScrollToIndexParams) => {
288
- const { horizontal } = recyclerViewManager.props;
289
- if (
290
- scrollViewRef.current &&
291
- index >= 0 &&
292
- index < recyclerViewManager.getDataLength()
293
- ) {
294
- // Pause the scroll offset adjustments
295
- pauseOffsetCorrection.current = true;
296
- recyclerViewManager.setOffsetProjectionEnabled(false);
297
-
298
- const getFinalOffset = () => {
299
- const layout = recyclerViewManager.getLayout(index);
300
- const offset = horizontal ? layout.x : layout.y;
301
- let finalOffset = offset;
302
- // take viewPosition etc into account
303
- if (viewPosition !== undefined || viewOffset !== undefined) {
304
- const containerSize = horizontal
305
- ? recyclerViewManager.getWindowSize().width
306
- : recyclerViewManager.getWindowSize().height;
307
-
308
- const itemSize = horizontal ? layout.width : layout.height;
309
-
310
- if (viewPosition !== undefined) {
311
- // viewPosition: 0 = top, 0.5 = center, 1 = bottom
312
- finalOffset =
313
- offset - (containerSize - itemSize) * viewPosition;
305
+ }: ScrollToIndexParams): Promise<void> => {
306
+ return new Promise((resolve) => {
307
+ const { horizontal } = recyclerViewManager.props;
308
+ if (
309
+ scrollViewRef.current &&
310
+ index >= 0 &&
311
+ index < recyclerViewManager.getDataLength()
312
+ ) {
313
+ // Pause the scroll offset adjustments
314
+ pauseOffsetCorrection.current = true;
315
+ recyclerViewManager.setOffsetProjectionEnabled(false);
316
+
317
+ const getFinalOffset = () => {
318
+ const layout = recyclerViewManager.getLayout(index);
319
+ const offset = horizontal ? layout.x : layout.y;
320
+ let finalOffset = offset;
321
+ // take viewPosition etc into account
322
+ if (viewPosition !== undefined || viewOffset !== undefined) {
323
+ const containerSize = horizontal
324
+ ? recyclerViewManager.getWindowSize().width
325
+ : recyclerViewManager.getWindowSize().height;
326
+
327
+ const itemSize = horizontal ? layout.width : layout.height;
328
+
329
+ if (viewPosition !== undefined) {
330
+ // viewPosition: 0 = top, 0.5 = center, 1 = bottom
331
+ finalOffset =
332
+ offset - (containerSize - itemSize) * viewPosition;
333
+ }
334
+
335
+ if (viewOffset !== undefined) {
336
+ finalOffset += viewOffset;
337
+ }
314
338
  }
315
-
316
- if (viewOffset !== undefined) {
317
- finalOffset += viewOffset;
339
+ return finalOffset + recyclerViewManager.firstItemOffset;
340
+ };
341
+ const lastAbsoluteScrollOffset =
342
+ recyclerViewManager.getAbsoluteLastScrollOffset();
343
+ const bufferForScroll = horizontal
344
+ ? recyclerViewManager.getWindowSize().width
345
+ : recyclerViewManager.getWindowSize().height;
346
+
347
+ const bufferForCompute = bufferForScroll * 2;
348
+
349
+ const getStartScrollOffset = () => {
350
+ let lastScrollOffset = lastAbsoluteScrollOffset;
351
+ const finalOffset = getFinalOffset();
352
+
353
+ if (finalOffset > lastScrollOffset) {
354
+ lastScrollOffset = Math.max(
355
+ finalOffset - bufferForCompute,
356
+ lastScrollOffset
357
+ );
358
+ recyclerViewManager.setScrollDirection("forward");
359
+ } else {
360
+ lastScrollOffset = Math.min(
361
+ finalOffset + bufferForCompute,
362
+ lastScrollOffset
363
+ );
364
+ recyclerViewManager.setScrollDirection("backward");
365
+ }
366
+ return lastScrollOffset;
367
+ };
368
+ let initialTargetOffset = getFinalOffset();
369
+ let initialStartScrollOffset = getStartScrollOffset();
370
+ let finalOffset = initialTargetOffset;
371
+ let startScrollOffset = initialStartScrollOffset;
372
+
373
+ const steps = 5;
374
+
375
+ /**
376
+ * Recursively performs the scroll animation steps.
377
+ * This function replaces the async/await loop with callback-based execution.
378
+ *
379
+ * @param currentStep - The current step in the animation (0 to steps-1)
380
+ */
381
+ const performScrollStep = (currentStep: number) => {
382
+ // Check if component is unmounted or we've completed all steps
383
+ if (isUnmounted.current) {
384
+ return;
385
+ } else if (currentStep >= steps) {
386
+ // All steps completed, perform final scroll
387
+ finishScrollToIndex();
388
+ return;
318
389
  }
319
- }
320
- return finalOffset + recyclerViewManager.firstItemOffset;
321
- };
322
- const lastAbsoluteScrollOffset =
323
- recyclerViewManager.getAbsoluteLastScrollOffset();
324
- const bufferForScroll = horizontal
325
- ? recyclerViewManager.getWindowSize().width
326
- : recyclerViewManager.getWindowSize().height;
327
-
328
- const bufferForCompute = bufferForScroll * 2;
329
-
330
- const getStartScrollOffset = () => {
331
- let lastScrollOffset = lastAbsoluteScrollOffset;
332
- const finalOffset = getFinalOffset();
333
-
334
- if (finalOffset > lastScrollOffset) {
335
- lastScrollOffset = Math.max(
336
- finalOffset - bufferForCompute,
337
- lastScrollOffset
338
- );
339
- recyclerViewManager.setScrollDirection("forward");
340
- } else {
341
- lastScrollOffset = Math.min(
342
- finalOffset + bufferForCompute,
343
- lastScrollOffset
344
- );
345
- recyclerViewManager.setScrollDirection("backward");
346
- }
347
- return lastScrollOffset;
348
- };
349
- let initialTargetOffset = getFinalOffset();
350
- let initialStartScrollOffset = getStartScrollOffset();
351
- let finalOffset = initialTargetOffset;
352
- let startScrollOffset = initialStartScrollOffset;
353
-
354
- const steps = 5;
355
- // go from finalOffset to startScrollOffset in 5 steps for animated
356
- // otherwise go from startScrollOffset to finalOffset in 5 steps
357
- for (let i = 0; i < steps; i++) {
358
- if (isUnmounted.current) {
359
- return;
360
- }
361
- const nextOffset = animated
362
- ? finalOffset +
363
- (startScrollOffset - finalOffset) * (i / (steps - 1))
364
- : startScrollOffset +
365
- (finalOffset - startScrollOffset) * (i / (steps - 1));
366
- await updateScrollOffsetAsync(nextOffset);
367
-
368
- // In case some change happens in the middle of this operation
369
- // and the index is out of bounds, scroll to the end
370
- if (index >= recyclerViewManager.getDataLength()) {
371
- handlerMethods.scrollToEnd({ animated });
372
- return;
373
- }
374
390
 
375
- const newFinalOffset = getFinalOffset();
376
- if (
377
- (newFinalOffset < initialTargetOffset &&
378
- newFinalOffset < initialStartScrollOffset) ||
379
- (newFinalOffset > initialTargetOffset &&
380
- newFinalOffset > initialStartScrollOffset)
381
- ) {
382
- finalOffset = newFinalOffset;
383
- startScrollOffset = getStartScrollOffset();
384
- initialTargetOffset = newFinalOffset;
385
- initialStartScrollOffset = startScrollOffset;
386
- i = -1; // Restart compute loop
387
- }
388
- }
391
+ // Calculate the offset for this step
392
+ // For animated scrolls: interpolate from finalOffset to startScrollOffset
393
+ // For non-animated: interpolate from startScrollOffset to finalOffset
394
+ const nextOffset = animated
395
+ ? finalOffset +
396
+ (startScrollOffset - finalOffset) *
397
+ (currentStep / (steps - 1))
398
+ : startScrollOffset +
399
+ (finalOffset - startScrollOffset) *
400
+ (currentStep / (steps - 1));
401
+
402
+ // Update scroll offset with a callback to continue to the next step
403
+ updateScrollOffsetWithCallback(nextOffset, () => {
404
+ // Check if the index is still valid after the update
405
+ if (index >= recyclerViewManager.getDataLength()) {
406
+ // Index out of bounds, scroll to end instead
407
+ handlerMethods.scrollToEnd({ animated });
408
+ resolve(); // Resolve the promise as we're done
409
+ return;
410
+ }
411
+
412
+ // Check if the target position has changed significantly
413
+ const newFinalOffset = getFinalOffset();
414
+ if (
415
+ (newFinalOffset < initialTargetOffset &&
416
+ newFinalOffset < initialStartScrollOffset) ||
417
+ (newFinalOffset > initialTargetOffset &&
418
+ newFinalOffset > initialStartScrollOffset)
419
+ ) {
420
+ // Target has moved, recalculate and restart from beginning
421
+ finalOffset = newFinalOffset;
422
+ startScrollOffset = getStartScrollOffset();
423
+ initialTargetOffset = newFinalOffset;
424
+ initialStartScrollOffset = startScrollOffset;
425
+ performScrollStep(0); // Restart from step 0
426
+ } else {
427
+ // Continue to next step
428
+ performScrollStep(currentStep + 1);
429
+ }
430
+ });
431
+ };
432
+
433
+ /**
434
+ * Completes the scroll to index operation by performing the final scroll
435
+ * and re-enabling offset correction after a delay.
436
+ */
437
+ const finishScrollToIndex = () => {
438
+ finalOffset = getFinalOffset();
439
+ const maxOffset = recyclerViewManager.getMaxScrollOffset();
440
+
441
+ if (finalOffset > maxOffset) {
442
+ finalOffset = maxOffset;
443
+ }
389
444
 
390
- finalOffset = getFinalOffset();
391
- const maxOffset = recyclerViewManager.getMaxScrollOffset();
445
+ if (animated) {
446
+ // For animated scrolls, first jump to the start position
447
+ // We don't need to add firstItemOffset here as it's already added
448
+ handlerMethods.scrollToOffset({
449
+ offset: startScrollOffset,
450
+ animated: false,
451
+ skipFirstItemOffset: true,
452
+ });
453
+ }
392
454
 
393
- if (finalOffset > maxOffset) {
394
- finalOffset = maxOffset;
395
- }
455
+ // Perform the final scroll to the target position
456
+ handlerMethods.scrollToOffset({
457
+ offset: finalOffset,
458
+ animated,
459
+ skipFirstItemOffset: true,
460
+ });
461
+
462
+ // Re-enable offset correction after a delay
463
+ // Longer delay for animated scrolls to allow animation to complete
464
+ setTimeout(
465
+ () => {
466
+ pauseOffsetCorrection.current = false;
467
+ recyclerViewManager.setOffsetProjectionEnabled(true);
468
+ resolve(); // Resolve the promise after re-enabling corrections
469
+ },
470
+ animated ? 300 : 200
471
+ );
472
+ };
396
473
 
397
- if (animated) {
398
- // We don't need to add firstItemOffset here as it's already added
399
- handlerMethods.scrollToOffset({
400
- offset: startScrollOffset,
401
- animated: false,
402
- skipFirstItemOffset: true,
403
- });
474
+ // Start the scroll animation process
475
+ performScrollStep(0);
476
+ } else {
477
+ // Invalid parameters, resolve immediately
478
+ resolve();
404
479
  }
405
- handlerMethods.scrollToOffset({
406
- offset: finalOffset,
407
- animated,
408
- skipFirstItemOffset: true,
409
- });
410
-
411
- setTimeout(
412
- () => {
413
- pauseOffsetCorrection.current = false;
414
- recyclerViewManager.setOffsetProjectionEnabled(true);
415
- },
416
- animated ? 300 : 200
417
- );
418
- }
480
+ });
419
481
  },
420
482
 
421
483
  /**
@@ -483,7 +545,7 @@ export function useRecyclerViewController<T>(
483
545
  scrollViewRef,
484
546
  setTimeout,
485
547
  isUnmounted,
486
- updateScrollOffsetAsync,
548
+ updateScrollOffsetWithCallback,
487
549
  ]);
488
550
 
489
551
  const applyInitialScrollIndex = useCallback(() => {
@@ -534,5 +596,10 @@ export function useRecyclerViewController<T>(
534
596
  [handlerMethods, scrollViewRef]
535
597
  );
536
598
 
537
- return { applyContentOffset, applyInitialScrollIndex, handlerMethods };
599
+ return {
600
+ applyOffsetCorrection,
601
+ computeFirstVisibleIndexForOffsetCorrection,
602
+ applyInitialScrollIndex,
603
+ handlerMethods,
604
+ };
538
605
  }
@@ -28,11 +28,13 @@ export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
28
28
  }, [data]);
29
29
 
30
30
  useEffect(() => {
31
+ recyclerViewManager.restoreIfNeeded();
32
+
31
33
  return () => {
32
34
  recyclerViewManager.dispose();
33
35
  velocityTracker.cleanUp();
34
36
  };
35
- // needs to run only on unmount
37
+ // Used to perform cleanup on unmount
36
38
  // eslint-disable-next-line react-hooks/exhaustive-deps
37
39
  }, []);
38
40
 
@@ -1,6 +1,10 @@
1
- import { Dispatch, SetStateAction, useCallback, useMemo, useRef } from "react";
1
+ import { useCallback, useMemo, useRef } from "react";
2
2
 
3
- import { useLayoutState } from "./useLayoutState";
3
+ import { LayoutStateSetter, useLayoutState } from "./useLayoutState";
4
+
5
+ export type RecyclingStateSetter<T> = LayoutStateSetter<T>;
6
+
7
+ export type RecyclingStateInitialValue<T> = T | (() => T);
4
8
 
5
9
  /**
6
10
  * A custom hook that provides state management with automatic reset functionality.
@@ -16,10 +20,10 @@ import { useLayoutState } from "./useLayoutState";
16
20
  * - A setState function that works like useState's setState
17
21
  */
18
22
  export function useRecyclingState<T>(
19
- initialState: T | (() => T),
23
+ initialState: RecyclingStateInitialValue<T>,
20
24
  deps: React.DependencyList,
21
25
  onReset?: () => void
22
- ): [T, Dispatch<SetStateAction<T>>] {
26
+ ): [T, RecyclingStateSetter<T>] {
23
27
  // Store the current state value in a ref to persist between renders
24
28
  const valueStore = useRef<T>();
25
29
  // Use layoutState to trigger re-renders when state changes
@@ -42,8 +46,8 @@ export function useRecyclingState<T>(
42
46
  * Proxy setState function that updates the stored value and triggers a re-render.
43
47
  * Only triggers a re-render if the new value is different from the current value.
44
48
  */
45
- const setStateProxy = useCallback(
46
- (newValue: T | ((prevValue: T) => T)) => {
49
+ const setStateProxy: RecyclingStateSetter<T> = useCallback(
50
+ (newValue, skipParentLayout) => {
47
51
  // Calculate next state value from function or direct value
48
52
  const nextState =
49
53
  typeof newValue === "function"
@@ -53,7 +57,7 @@ export function useRecyclingState<T>(
53
57
  // Only update and trigger re-render if value has changed
54
58
  if (nextState !== valueStore.current) {
55
59
  valueStore.current = nextState;
56
- setCounter((prev) => prev + 1);
60
+ setCounter((prev) => prev + 1, skipParentLayout);
57
61
  }
58
62
  },
59
63
  [setCounter]