@shopify/flash-list 2.0.0-rc.2 → 2.0.0-rc.4

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 (66) hide show
  1. package/dist/FlashListProps.d.ts +10 -2
  2. package/dist/FlashListProps.d.ts.map +1 -1
  3. package/dist/FlashListProps.js.map +1 -1
  4. package/dist/__tests__/RenderStackManager.test.js +1 -2
  5. package/dist/__tests__/RenderStackManager.test.js.map +1 -1
  6. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  7. package/dist/recyclerview/RecyclerView.js +11 -3
  8. package/dist/recyclerview/RecyclerView.js.map +1 -1
  9. package/dist/recyclerview/RecyclerViewManager.d.ts +1 -0
  10. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
  11. package/dist/recyclerview/RecyclerViewManager.js +5 -0
  12. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  13. package/dist/recyclerview/RenderStackManager.d.ts +1 -0
  14. package/dist/recyclerview/RenderStackManager.d.ts.map +1 -1
  15. package/dist/recyclerview/RenderStackManager.js +26 -7
  16. package/dist/recyclerview/RenderStackManager.js.map +1 -1
  17. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts +1 -0
  18. package/dist/recyclerview/helpers/RenderTimeTracker.d.ts.map +1 -1
  19. package/dist/recyclerview/helpers/RenderTimeTracker.js +3 -0
  20. package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -1
  21. package/dist/recyclerview/hooks/useLayoutState.d.ts +3 -1
  22. package/dist/recyclerview/hooks/useLayoutState.d.ts.map +1 -1
  23. package/dist/recyclerview/hooks/useLayoutState.js +5 -3
  24. package/dist/recyclerview/hooks/useLayoutState.js.map +1 -1
  25. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +2 -1
  26. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  27. package/dist/recyclerview/hooks/useRecyclerViewController.js +237 -190
  28. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  29. package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
  30. package/dist/recyclerview/hooks/useRecyclerViewManager.js +2 -1
  31. package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
  32. package/dist/recyclerview/hooks/useRecyclingState.d.ts +4 -2
  33. package/dist/recyclerview/hooks/useRecyclingState.d.ts.map +1 -1
  34. package/dist/recyclerview/hooks/useRecyclingState.js +2 -2
  35. package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -1
  36. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +14 -6
  37. package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
  38. package/dist/recyclerview/layout-managers/GridLayoutManager.js +40 -23
  39. package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
  40. package/dist/recyclerview/layout-managers/LayoutManager.d.ts +26 -6
  41. package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
  42. package/dist/recyclerview/layout-managers/LayoutManager.js +89 -15
  43. package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
  44. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts +9 -1
  45. package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -1
  46. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +28 -12
  47. package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
  48. package/dist/tsconfig.tsbuildinfo +1 -1
  49. package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
  50. package/dist/viewability/ViewabilityManager.js +10 -3
  51. package/dist/viewability/ViewabilityManager.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/FlashListProps.ts +16 -2
  54. package/src/__tests__/RenderStackManager.test.ts +1 -2
  55. package/src/recyclerview/RecyclerView.tsx +27 -14
  56. package/src/recyclerview/RecyclerViewManager.ts +6 -0
  57. package/src/recyclerview/RenderStackManager.ts +32 -6
  58. package/src/recyclerview/helpers/RenderTimeTracker.ts +4 -0
  59. package/src/recyclerview/hooks/useLayoutState.ts +15 -6
  60. package/src/recyclerview/hooks/useRecyclerViewController.tsx +240 -168
  61. package/src/recyclerview/hooks/useRecyclerViewManager.ts +3 -1
  62. package/src/recyclerview/hooks/useRecyclingState.ts +11 -7
  63. package/src/recyclerview/layout-managers/GridLayoutManager.ts +44 -23
  64. package/src/recyclerview/layout-managers/LayoutManager.ts +98 -20
  65. package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +30 -8
  66. package/src/viewability/ViewabilityManager.ts +10 -6
@@ -51,51 +51,84 @@ export function useRecyclerViewController<T>(
51
51
  const [_, setRenderId] = useState(0);
52
52
  const pauseOffsetCorrection = useRef(false);
53
53
  const initialScrollCompletedRef = useRef(false);
54
- const lastDataLengthRef = useRef(recyclerViewManager.props.data?.length ?? 0);
54
+ const lastDataLengthRef = useRef(recyclerViewManager.getDataLength());
55
55
  const { setTimeout } = useUnmountAwareTimeout();
56
56
 
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 (
95
+ recyclerViewManager.getIsFirstLayoutComplete() &&
96
+ keyExtractor &&
97
+ recyclerViewManager.getDataLength() > 0 &&
98
+ recyclerViewManager.shouldMaintainVisibleContentPosition()
99
+ ) {
100
+ // Update the tracked first visible item
101
+ const firstVisibleIndex = Math.max(
102
+ 0,
103
+ recyclerViewManager.computeVisibleIndices().startIndex
104
+ );
105
+ if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
106
+ firstVisibleItemKey.current = keyExtractor(
107
+ data![firstVisibleIndex],
108
+ firstVisibleIndex
109
+ );
110
+ firstVisibleItemLayout.current = {
111
+ ...recyclerViewManager.getLayout(firstVisibleIndex),
112
+ };
113
+ }
114
+ }
115
+ }, [recyclerViewManager]);
116
+
86
117
  /**
87
118
  * Maintains the visible content position when the list updates.
88
119
  * This is particularly useful for chat applications where we want to keep
89
120
  * the user's current view position when new messages are added.
90
121
  */
91
- const applyContentOffset = useCallback(async () => {
122
+ const applyOffsetCorrection = useCallback(() => {
92
123
  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());
97
124
 
98
- const currentDataLength = data?.length ?? 0;
125
+ // Execute all pending callbacks from previous scroll offset updates
126
+ // This ensures any scroll operations that were waiting for render are completed
127
+ const callbacks = pendingScrollCallbacks.current;
128
+ pendingScrollCallbacks.current = [];
129
+ callbacks.forEach((callback) => callback());
130
+
131
+ const currentDataLength = recyclerViewManager.getDataLength();
99
132
 
100
133
  if (
101
134
  recyclerViewManager.getIsFirstLayoutComplete() &&
@@ -152,8 +185,9 @@ export function useRecyclerViewController<T>(
152
185
  scrollViewRef.current?.scrollTo(scrollToParams);
153
186
  }
154
187
  if (hasDataChanged) {
155
- updateScrollOffsetAsync(
156
- recyclerViewManager.getAbsoluteLastScrollOffset() + diff
188
+ updateScrollOffsetWithCallback(
189
+ recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
190
+ () => {}
157
191
  );
158
192
  recyclerViewManager.ignoreScrollEvents = true;
159
193
  setTimeout(() => {
@@ -164,28 +198,16 @@ export function useRecyclerViewController<T>(
164
198
  }
165
199
  }
166
200
 
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
- }
201
+ computeFirstVisibleIndexForOffsetCorrection();
181
202
  }
182
- lastDataLengthRef.current = data?.length ?? 0;
203
+ lastDataLengthRef.current = recyclerViewManager.getDataLength();
183
204
  }, [
184
205
  recyclerViewManager,
185
206
  scrollAnchorRef,
186
207
  scrollViewRef,
187
208
  setTimeout,
188
- updateScrollOffsetAsync,
209
+ updateScrollOffsetWithCallback,
210
+ computeFirstVisibleIndexForOffsetCorrection,
189
211
  ]);
190
212
 
191
213
  const handlerMethods: FlashListRef<T> = useMemo(() => {
@@ -278,144 +300,189 @@ export function useRecyclerViewController<T>(
278
300
  /**
279
301
  * Scrolls to a specific index in the list.
280
302
  * Supports viewPosition and viewOffset for precise positioning.
303
+ * Returns a Promise that resolves when the scroll is complete.
281
304
  */
282
- scrollToIndex: async ({
305
+ scrollToIndex: ({
283
306
  index,
284
307
  animated,
285
308
  viewPosition,
286
309
  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;
310
+ }: ScrollToIndexParams): Promise<void> => {
311
+ return new Promise((resolve) => {
312
+ const { horizontal } = recyclerViewManager.props;
313
+ if (
314
+ scrollViewRef.current &&
315
+ index >= 0 &&
316
+ index < recyclerViewManager.getDataLength()
317
+ ) {
318
+ // Pause the scroll offset adjustments
319
+ pauseOffsetCorrection.current = true;
320
+ recyclerViewManager.setOffsetProjectionEnabled(false);
321
+
322
+ const getFinalOffset = () => {
323
+ const layout = recyclerViewManager.getLayout(index);
324
+ const offset = horizontal ? layout.x : layout.y;
325
+ let finalOffset = offset;
326
+ // take viewPosition etc into account
327
+ if (viewPosition !== undefined || viewOffset !== undefined) {
328
+ const containerSize = horizontal
329
+ ? recyclerViewManager.getWindowSize().width
330
+ : recyclerViewManager.getWindowSize().height;
331
+
332
+ const itemSize = horizontal ? layout.width : layout.height;
333
+
334
+ if (viewPosition !== undefined) {
335
+ // viewPosition: 0 = top, 0.5 = center, 1 = bottom
336
+ finalOffset =
337
+ offset - (containerSize - itemSize) * viewPosition;
338
+ }
339
+
340
+ if (viewOffset !== undefined) {
341
+ finalOffset += viewOffset;
342
+ }
314
343
  }
315
-
316
- if (viewOffset !== undefined) {
317
- finalOffset += viewOffset;
344
+ return finalOffset + recyclerViewManager.firstItemOffset;
345
+ };
346
+ const lastAbsoluteScrollOffset =
347
+ recyclerViewManager.getAbsoluteLastScrollOffset();
348
+ const bufferForScroll = horizontal
349
+ ? recyclerViewManager.getWindowSize().width
350
+ : recyclerViewManager.getWindowSize().height;
351
+
352
+ const bufferForCompute = bufferForScroll * 2;
353
+
354
+ const getStartScrollOffset = () => {
355
+ let lastScrollOffset = lastAbsoluteScrollOffset;
356
+ const finalOffset = getFinalOffset();
357
+
358
+ if (finalOffset > lastScrollOffset) {
359
+ lastScrollOffset = Math.max(
360
+ finalOffset - bufferForCompute,
361
+ lastScrollOffset
362
+ );
363
+ recyclerViewManager.setScrollDirection("forward");
364
+ } else {
365
+ lastScrollOffset = Math.min(
366
+ finalOffset + bufferForCompute,
367
+ lastScrollOffset
368
+ );
369
+ recyclerViewManager.setScrollDirection("backward");
370
+ }
371
+ return lastScrollOffset;
372
+ };
373
+ let initialTargetOffset = getFinalOffset();
374
+ let initialStartScrollOffset = getStartScrollOffset();
375
+ let finalOffset = initialTargetOffset;
376
+ let startScrollOffset = initialStartScrollOffset;
377
+
378
+ const steps = 5;
379
+
380
+ /**
381
+ * Recursively performs the scroll animation steps.
382
+ * This function replaces the async/await loop with callback-based execution.
383
+ *
384
+ * @param currentStep - The current step in the animation (0 to steps-1)
385
+ */
386
+ const performScrollStep = (currentStep: number) => {
387
+ // Check if component is unmounted or we've completed all steps
388
+ if (isUnmounted.current) {
389
+ return;
390
+ } else if (currentStep >= steps) {
391
+ // All steps completed, perform final scroll
392
+ finishScrollToIndex();
393
+ return;
318
394
  }
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
395
 
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
- }
396
+ // Calculate the offset for this step
397
+ // For animated scrolls: interpolate from finalOffset to startScrollOffset
398
+ // For non-animated: interpolate from startScrollOffset to finalOffset
399
+ const nextOffset = animated
400
+ ? finalOffset +
401
+ (startScrollOffset - finalOffset) *
402
+ (currentStep / (steps - 1))
403
+ : startScrollOffset +
404
+ (finalOffset - startScrollOffset) *
405
+ (currentStep / (steps - 1));
406
+
407
+ // Update scroll offset with a callback to continue to the next step
408
+ updateScrollOffsetWithCallback(nextOffset, () => {
409
+ // Check if the index is still valid after the update
410
+ if (index >= recyclerViewManager.getDataLength()) {
411
+ // Index out of bounds, scroll to end instead
412
+ handlerMethods.scrollToEnd({ animated });
413
+ resolve(); // Resolve the promise as we're done
414
+ return;
415
+ }
416
+
417
+ // Check if the target position has changed significantly
418
+ const newFinalOffset = getFinalOffset();
419
+ if (
420
+ (newFinalOffset < initialTargetOffset &&
421
+ newFinalOffset < initialStartScrollOffset) ||
422
+ (newFinalOffset > initialTargetOffset &&
423
+ newFinalOffset > initialStartScrollOffset)
424
+ ) {
425
+ // Target has moved, recalculate and restart from beginning
426
+ finalOffset = newFinalOffset;
427
+ startScrollOffset = getStartScrollOffset();
428
+ initialTargetOffset = newFinalOffset;
429
+ initialStartScrollOffset = startScrollOffset;
430
+ performScrollStep(0); // Restart from step 0
431
+ } else {
432
+ // Continue to next step
433
+ performScrollStep(currentStep + 1);
434
+ }
435
+ });
436
+ };
437
+
438
+ /**
439
+ * Completes the scroll to index operation by performing the final scroll
440
+ * and re-enabling offset correction after a delay.
441
+ */
442
+ const finishScrollToIndex = () => {
443
+ finalOffset = getFinalOffset();
444
+ const maxOffset = recyclerViewManager.getMaxScrollOffset();
445
+
446
+ if (finalOffset > maxOffset) {
447
+ finalOffset = maxOffset;
448
+ }
389
449
 
390
- finalOffset = getFinalOffset();
391
- const maxOffset = recyclerViewManager.getMaxScrollOffset();
450
+ if (animated) {
451
+ // For animated scrolls, first jump to the start position
452
+ // We don't need to add firstItemOffset here as it's already added
453
+ handlerMethods.scrollToOffset({
454
+ offset: startScrollOffset,
455
+ animated: false,
456
+ skipFirstItemOffset: true,
457
+ });
458
+ }
392
459
 
393
- if (finalOffset > maxOffset) {
394
- finalOffset = maxOffset;
395
- }
460
+ // Perform the final scroll to the target position
461
+ handlerMethods.scrollToOffset({
462
+ offset: finalOffset,
463
+ animated,
464
+ skipFirstItemOffset: true,
465
+ });
466
+
467
+ // Re-enable offset correction after a delay
468
+ // Longer delay for animated scrolls to allow animation to complete
469
+ setTimeout(
470
+ () => {
471
+ pauseOffsetCorrection.current = false;
472
+ recyclerViewManager.setOffsetProjectionEnabled(true);
473
+ resolve(); // Resolve the promise after re-enabling corrections
474
+ },
475
+ animated ? 300 : 200
476
+ );
477
+ };
396
478
 
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
- });
479
+ // Start the scroll animation process
480
+ performScrollStep(0);
481
+ } else {
482
+ // Invalid parameters, resolve immediately
483
+ resolve();
404
484
  }
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
- }
485
+ });
419
486
  },
420
487
 
421
488
  /**
@@ -483,7 +550,7 @@ export function useRecyclerViewController<T>(
483
550
  scrollViewRef,
484
551
  setTimeout,
485
552
  isUnmounted,
486
- updateScrollOffsetAsync,
553
+ updateScrollOffsetWithCallback,
487
554
  ]);
488
555
 
489
556
  const applyInitialScrollIndex = useCallback(() => {
@@ -534,5 +601,10 @@ export function useRecyclerViewController<T>(
534
601
  [handlerMethods, scrollViewRef]
535
602
  );
536
603
 
537
- return { applyContentOffset, applyInitialScrollIndex, handlerMethods };
604
+ return {
605
+ applyOffsetCorrection,
606
+ computeFirstVisibleIndexForOffsetCorrection,
607
+ applyInitialScrollIndex,
608
+ handlerMethods,
609
+ };
538
610
  }
@@ -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]