@shopify/flash-list 2.0.0-alpha.8 → 2.0.0-rc.1
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 +38 -98
- 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 +15 -8
- 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/MasonryFlashList.js.map +1 -1
- package/dist/__tests__/RecyclerView.test.js +63 -28
- 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 +486 -0
- package/dist/__tests__/RenderStackManager.test.js.map +1 -0
- package/dist/__tests__/helpers/createLayoutManager.d.ts.map +1 -1
- package/dist/__tests__/helpers/createLayoutManager.js +3 -4
- package/dist/__tests__/helpers/createLayoutManager.js.map +1 -1
- 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/enableNewCore.d.ts.map +1 -1
- package/dist/enableNewCore.js +2 -1
- package/dist/enableNewCore.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/native/config/PlatformHelper.android.d.ts +2 -0
- package/dist/native/config/PlatformHelper.android.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.android.js +2 -0
- package/dist/native/config/PlatformHelper.android.js.map +1 -1
- package/dist/native/config/PlatformHelper.d.ts +2 -0
- package/dist/native/config/PlatformHelper.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.ios.d.ts +2 -0
- package/dist/native/config/PlatformHelper.ios.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.ios.js +2 -0
- package/dist/native/config/PlatformHelper.ios.js.map +1 -1
- package/dist/native/config/PlatformHelper.js +2 -0
- package/dist/native/config/PlatformHelper.js.map +1 -1
- package/dist/native/config/PlatformHelper.web.d.ts +2 -0
- package/dist/native/config/PlatformHelper.web.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.web.js +3 -1
- package/dist/native/config/PlatformHelper.web.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 +64 -37
- 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 +21 -7
- package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerViewManager.js +105 -113
- 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 +324 -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 +23 -8
- 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 +40 -32
- package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
- package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts +45 -1
- package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts.map +1 -1
- package/dist/recyclerview/helpers/EngagedIndicesTracker.js +79 -20
- package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -1
- package/dist/recyclerview/helpers/RenderTimeTracker.d.ts +10 -0
- package/dist/recyclerview/helpers/RenderTimeTracker.d.ts.map +1 -0
- package/dist/recyclerview/helpers/RenderTimeTracker.js +39 -0
- package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -0
- package/dist/recyclerview/helpers/VelocityTracker.d.ts +29 -0
- package/dist/recyclerview/helpers/VelocityTracker.d.ts.map +1 -0
- package/dist/recyclerview/helpers/VelocityTracker.js +70 -0
- package/dist/recyclerview/helpers/VelocityTracker.js.map +1 -0
- 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/useMappingHelper.d.ts +1 -1
- package/dist/recyclerview/hooks/useMappingHelper.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useMappingHelper.js +1 -1
- package/dist/recyclerview/hooks/useMappingHelper.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 +179 -119
- package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts +2 -0
- package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewManager.js +10 -1
- 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 +10 -16
- package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/LayoutManager.js +4 -14
- package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +2 -2
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
- package/dist/recyclerview/utils/measureLayout.d.ts +1 -29
- package/dist/recyclerview/utils/measureLayout.d.ts.map +1 -1
- package/dist/recyclerview/utils/measureLayout.js +2 -4
- package/dist/recyclerview/utils/measureLayout.js.map +1 -1
- package/dist/recyclerview/utils/measureLayout.web.d.ts +29 -0
- package/dist/recyclerview/utils/measureLayout.web.d.ts.map +1 -0
- package/dist/recyclerview/utils/measureLayout.web.js +89 -0
- package/dist/recyclerview/utils/measureLayout.web.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/viewability/ViewToken.d.ts +2 -2
- package/dist/viewability/ViewToken.d.ts.map +1 -1
- package/dist/viewability/ViewabilityHelper.js +1 -1
- package/dist/viewability/ViewabilityHelper.js.map +1 -1
- package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
- package/dist/viewability/ViewabilityManager.js +1 -2
- package/dist/viewability/ViewabilityManager.js.map +1 -1
- package/jestSetup.js +30 -11
- package/package.json +2 -1
- package/src/AnimatedFlashList.ts +3 -2
- package/src/FlashList.tsx +24 -0
- package/src/FlashListProps.ts +20 -8
- package/src/FlashListRef.ts +320 -0
- package/src/MasonryFlashList.tsx +2 -2
- package/src/__tests__/RecyclerView.test.tsx +84 -30
- package/src/__tests__/RenderStackManager.test.ts +575 -0
- package/src/__tests__/helpers/createLayoutManager.ts +2 -3
- package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
- package/src/benchmark/useFlatListBenchmark.ts +2 -2
- package/src/enableNewCore.ts +3 -1
- package/src/index.ts +1 -0
- package/src/native/config/PlatformHelper.android.ts +2 -0
- package/src/native/config/PlatformHelper.ios.ts +2 -0
- package/src/native/config/PlatformHelper.ts +2 -0
- package/src/native/config/PlatformHelper.web.ts +3 -1
- package/src/recyclerview/RecyclerView.tsx +85 -43
- package/src/recyclerview/RecyclerViewContextProvider.ts +12 -6
- package/src/recyclerview/RecyclerViewManager.ts +123 -98
- package/src/recyclerview/RenderStackManager.ts +291 -0
- package/src/recyclerview/ViewHolder.tsx +5 -3
- package/src/recyclerview/ViewHolderCollection.tsx +33 -12
- package/src/recyclerview/components/ScrollAnchor.tsx +21 -8
- package/src/recyclerview/components/StickyHeaders.tsx +63 -44
- package/src/recyclerview/helpers/EngagedIndicesTracker.ts +120 -23
- package/src/recyclerview/helpers/RenderTimeTracker.ts +38 -0
- package/src/recyclerview/helpers/VelocityTracker.ts +77 -0
- package/src/recyclerview/hooks/useBoundDetection.ts +25 -18
- package/src/recyclerview/hooks/useMappingHelper.ts +1 -1
- package/src/recyclerview/hooks/useOnLoad.ts +4 -6
- package/src/recyclerview/hooks/useRecyclerViewController.tsx +204 -173
- package/src/recyclerview/hooks/useRecyclerViewManager.ts +11 -1
- 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 +12 -21
- package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +1 -1
- package/src/recyclerview/utils/measureLayout.ts +3 -3
- package/src/recyclerview/utils/measureLayout.web.ts +104 -0
- package/src/viewability/ViewToken.ts +2 -2
- package/src/viewability/ViewabilityHelper.ts +1 -1
- package/src/viewability/ViewabilityManager.ts +6 -3
- 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
|
@@ -8,66 +8,22 @@ import {
|
|
|
8
8
|
} from "react";
|
|
9
9
|
import { I18nManager } from "react-native";
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
ScrollToOffsetParams,
|
|
13
|
+
ScrollToIndexParams,
|
|
14
|
+
ScrollToItemParams,
|
|
15
|
+
ScrollToEdgeParams,
|
|
16
|
+
FlashListRef,
|
|
17
|
+
} from "../../FlashListRef";
|
|
12
18
|
import { CompatScroller } from "../components/CompatScroller";
|
|
13
19
|
import { RecyclerViewManager } from "../RecyclerViewManager";
|
|
14
20
|
import { adjustOffsetForRTL } from "../utils/adjustOffsetForRTL";
|
|
15
21
|
import { RVLayout } from "../layout-managers/LayoutManager";
|
|
16
22
|
import { ScrollAnchorRef } from "../components/ScrollAnchor";
|
|
23
|
+
import { PlatformConfig } from "../../native/config/PlatformHelper";
|
|
17
24
|
|
|
18
25
|
import { useUnmountFlag } from "./useUnmountFlag";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Parameters for scrolling to a specific position in the list.
|
|
23
|
-
* Extends ScrollToEdgeParams to include view positioning options.
|
|
24
|
-
*/
|
|
25
|
-
export interface ScrollToParams extends ScrollToEdgeParams {
|
|
26
|
-
/** Position of the target item relative to the viewport (0 = top, 0.5 = center, 1 = bottom) */
|
|
27
|
-
viewPosition?: number;
|
|
28
|
-
/** Additional offset to apply after viewPosition calculation */
|
|
29
|
-
viewOffset?: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Parameters for scrolling to a specific offset in the list.
|
|
34
|
-
* Used when you want to scroll to an exact pixel position.
|
|
35
|
-
*/
|
|
36
|
-
export interface ScrollToOffsetParams extends ScrollToParams {
|
|
37
|
-
/** The pixel offset to scroll to */
|
|
38
|
-
offset: number;
|
|
39
|
-
/**
|
|
40
|
-
* If true, the first item offset will not be added to the offset calculation.
|
|
41
|
-
* First offset represents header size or top padding.
|
|
42
|
-
*/
|
|
43
|
-
skipFirstItemOffset?: boolean;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Parameters for scrolling to a specific index in the list.
|
|
48
|
-
* Used when you want to scroll to a specific item by its position in the data array.
|
|
49
|
-
*/
|
|
50
|
-
export interface ScrollToIndexParams extends ScrollToParams {
|
|
51
|
-
/** The index of the item to scroll to */
|
|
52
|
-
index: number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Parameters for scrolling to a specific item in the list.
|
|
57
|
-
* Used when you want to scroll to a specific item by its data value.
|
|
58
|
-
*/
|
|
59
|
-
export interface ScrollToItemParams<T> extends ScrollToParams {
|
|
60
|
-
/** The item to scroll to */
|
|
61
|
-
item: T;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Base parameters for scrolling to the edges of the list.
|
|
66
|
-
*/
|
|
67
|
-
export interface ScrollToEdgeParams {
|
|
68
|
-
/** Whether the scroll should be animated */
|
|
69
|
-
animated?: boolean;
|
|
70
|
-
}
|
|
26
|
+
import { useUnmountAwareTimeout } from "./useUnmountAwareCallbacks";
|
|
71
27
|
|
|
72
28
|
/**
|
|
73
29
|
* Comprehensive hook that manages RecyclerView scrolling behavior and provides
|
|
@@ -87,61 +43,22 @@ export interface ScrollToEdgeParams {
|
|
|
87
43
|
*/
|
|
88
44
|
export function useRecyclerViewController<T>(
|
|
89
45
|
recyclerViewManager: RecyclerViewManager<T>,
|
|
90
|
-
ref: React.Ref<
|
|
46
|
+
ref: React.Ref<FlashListRef<T>>,
|
|
91
47
|
scrollViewRef: RefObject<CompatScroller>,
|
|
92
|
-
scrollAnchorRef: React.RefObject<ScrollAnchorRef
|
|
93
|
-
props: RecyclerViewProps<T>
|
|
48
|
+
scrollAnchorRef: React.RefObject<ScrollAnchorRef>
|
|
94
49
|
) {
|
|
95
|
-
const { horizontal, data } = props;
|
|
96
50
|
const isUnmounted = useUnmountFlag();
|
|
97
51
|
const [_, setRenderId] = useState(0);
|
|
98
|
-
const
|
|
52
|
+
const pauseOffsetCorrection = useRef(false);
|
|
99
53
|
const initialScrollCompletedRef = useRef(false);
|
|
100
|
-
const lastDataLengthRef = useRef(data?.length ?? 0);
|
|
101
|
-
const { setTimeout } =
|
|
54
|
+
const lastDataLengthRef = useRef(recyclerViewManager.props.data?.length ?? 0);
|
|
55
|
+
const { setTimeout } = useUnmountAwareTimeout();
|
|
102
56
|
|
|
103
57
|
// Track the first visible item for maintaining scroll position
|
|
104
58
|
const firstVisibleItemKey = useRef<string | undefined>(undefined);
|
|
105
59
|
const firstVisibleItemLayout = useRef<RVLayout | undefined>(undefined);
|
|
106
60
|
const pendingScrollResolves = useRef<(() => void)[]>([]);
|
|
107
61
|
|
|
108
|
-
const applyInitialScrollIndex = useCallback(() => {
|
|
109
|
-
const initialScrollIndex =
|
|
110
|
-
recyclerViewManager.getInitialScrollIndex() ?? -1;
|
|
111
|
-
const dataLength = props.data?.length ?? 0;
|
|
112
|
-
if (
|
|
113
|
-
initialScrollIndex >= 0 &&
|
|
114
|
-
initialScrollIndex < dataLength &&
|
|
115
|
-
!initialScrollCompletedRef.current &&
|
|
116
|
-
recyclerViewManager.getIsFirstLayoutComplete()
|
|
117
|
-
) {
|
|
118
|
-
// Use setTimeout to ensure that we keep trying to scroll on first few renders
|
|
119
|
-
setTimeout(() => {
|
|
120
|
-
initialScrollCompletedRef.current = true;
|
|
121
|
-
pauseAdjustRef.current = false;
|
|
122
|
-
}, 100);
|
|
123
|
-
|
|
124
|
-
pauseAdjustRef.current = true;
|
|
125
|
-
|
|
126
|
-
const offset = horizontal
|
|
127
|
-
? recyclerViewManager.getLayout(initialScrollIndex).x
|
|
128
|
-
: recyclerViewManager.getLayout(initialScrollIndex).y;
|
|
129
|
-
handlerMethods.scrollToOffset({
|
|
130
|
-
offset,
|
|
131
|
-
animated: false,
|
|
132
|
-
skipFirstItemOffset: false,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
setTimeout(() => {
|
|
136
|
-
handlerMethods.scrollToOffset({
|
|
137
|
-
offset,
|
|
138
|
-
animated: false,
|
|
139
|
-
skipFirstItemOffset: false,
|
|
140
|
-
});
|
|
141
|
-
}, 0);
|
|
142
|
-
}
|
|
143
|
-
}, [recyclerViewManager, props.data]);
|
|
144
|
-
|
|
145
62
|
// Handle initial scroll position when the list first loads
|
|
146
63
|
// useOnLoad(recyclerViewManager, () => {
|
|
147
64
|
|
|
@@ -172,19 +89,19 @@ export function useRecyclerViewController<T>(
|
|
|
172
89
|
* the user's current view position when new messages are added.
|
|
173
90
|
*/
|
|
174
91
|
const applyContentOffset = useCallback(async () => {
|
|
92
|
+
const { horizontal, data, keyExtractor } = recyclerViewManager.props;
|
|
175
93
|
// Resolve all pending scroll updates from previous calls
|
|
176
94
|
const resolves = pendingScrollResolves.current;
|
|
177
95
|
pendingScrollResolves.current = [];
|
|
178
96
|
resolves.forEach((resolve) => resolve());
|
|
179
97
|
|
|
180
|
-
const currentDataLength =
|
|
98
|
+
const currentDataLength = data?.length ?? 0;
|
|
181
99
|
|
|
182
100
|
if (
|
|
183
|
-
!props.horizontal &&
|
|
184
101
|
recyclerViewManager.getIsFirstLayoutComplete() &&
|
|
185
|
-
|
|
102
|
+
keyExtractor &&
|
|
186
103
|
currentDataLength > 0 &&
|
|
187
|
-
|
|
104
|
+
recyclerViewManager.shouldMaintainVisibleContentPosition()
|
|
188
105
|
) {
|
|
189
106
|
const hasDataChanged = currentDataLength !== lastDataLengthRef.current;
|
|
190
107
|
// If we have a tracked first visible item, maintain its position
|
|
@@ -194,28 +111,46 @@ export function useRecyclerViewController<T>(
|
|
|
194
111
|
.getEngagedIndices()
|
|
195
112
|
.findValue(
|
|
196
113
|
(index) =>
|
|
197
|
-
|
|
114
|
+
keyExtractor?.(data![index], index) ===
|
|
198
115
|
firstVisibleItemKey.current
|
|
199
116
|
) ??
|
|
200
117
|
(hasDataChanged
|
|
201
|
-
?
|
|
118
|
+
? data?.findIndex(
|
|
202
119
|
(item, index) =>
|
|
203
|
-
|
|
204
|
-
firstVisibleItemKey.current
|
|
120
|
+
keyExtractor?.(item, index) === firstVisibleItemKey.current
|
|
205
121
|
)
|
|
206
122
|
: undefined);
|
|
207
123
|
|
|
208
|
-
if (
|
|
124
|
+
if (
|
|
125
|
+
currentIndexOfFirstVisibleItem !== undefined &&
|
|
126
|
+
currentIndexOfFirstVisibleItem >= 0
|
|
127
|
+
) {
|
|
209
128
|
// Calculate the difference in position and apply the offset
|
|
210
|
-
const diff =
|
|
211
|
-
recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem).
|
|
212
|
-
|
|
129
|
+
const diff = horizontal
|
|
130
|
+
? recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem).x -
|
|
131
|
+
firstVisibleItemLayout.current!.x
|
|
132
|
+
: recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem).y -
|
|
133
|
+
firstVisibleItemLayout.current!.y;
|
|
213
134
|
firstVisibleItemLayout.current = {
|
|
214
135
|
...recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem),
|
|
215
136
|
};
|
|
216
|
-
if (diff !== 0 && !
|
|
137
|
+
if (diff !== 0 && !pauseOffsetCorrection.current) {
|
|
217
138
|
// console.log("diff", diff, firstVisibleItemKey.current);
|
|
218
|
-
|
|
139
|
+
if (PlatformConfig.supportsOffsetCorrection) {
|
|
140
|
+
// console.log("scrollBy", diff);
|
|
141
|
+
scrollAnchorRef.current?.scrollBy(diff);
|
|
142
|
+
} else {
|
|
143
|
+
const scrollToParams = horizontal
|
|
144
|
+
? {
|
|
145
|
+
x: recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
|
|
146
|
+
animated: false,
|
|
147
|
+
}
|
|
148
|
+
: {
|
|
149
|
+
y: recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
|
|
150
|
+
animated: false,
|
|
151
|
+
};
|
|
152
|
+
scrollViewRef.current?.scrollTo(scrollToParams);
|
|
153
|
+
}
|
|
219
154
|
if (hasDataChanged) {
|
|
220
155
|
updateScrollOffsetAsync(
|
|
221
156
|
recyclerViewManager.getAbsoluteLastScrollOffset() + diff
|
|
@@ -232,11 +167,11 @@ export function useRecyclerViewController<T>(
|
|
|
232
167
|
// Update the tracked first visible item
|
|
233
168
|
const firstVisibleIndex = Math.max(
|
|
234
169
|
0,
|
|
235
|
-
recyclerViewManager.
|
|
170
|
+
recyclerViewManager.computeVisibleIndices().startIndex
|
|
236
171
|
);
|
|
237
172
|
if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
|
|
238
|
-
firstVisibleItemKey.current =
|
|
239
|
-
|
|
173
|
+
firstVisibleItemKey.current = keyExtractor(
|
|
174
|
+
data![firstVisibleIndex],
|
|
240
175
|
firstVisibleIndex
|
|
241
176
|
);
|
|
242
177
|
firstVisibleItemLayout.current = {
|
|
@@ -244,12 +179,20 @@ export function useRecyclerViewController<T>(
|
|
|
244
179
|
};
|
|
245
180
|
}
|
|
246
181
|
}
|
|
247
|
-
lastDataLengthRef.current =
|
|
248
|
-
}, [
|
|
249
|
-
|
|
250
|
-
|
|
182
|
+
lastDataLengthRef.current = data?.length ?? 0;
|
|
183
|
+
}, [
|
|
184
|
+
recyclerViewManager,
|
|
185
|
+
scrollAnchorRef,
|
|
186
|
+
scrollViewRef,
|
|
187
|
+
setTimeout,
|
|
188
|
+
updateScrollOffsetAsync,
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
const handlerMethods: FlashListRef<T> = useMemo(() => {
|
|
251
192
|
return {
|
|
252
|
-
props
|
|
193
|
+
get props() {
|
|
194
|
+
return recyclerViewManager.props;
|
|
195
|
+
},
|
|
253
196
|
/**
|
|
254
197
|
* Scrolls the list to a specific offset position.
|
|
255
198
|
* Handles RTL layouts and first item offset adjustments.
|
|
@@ -259,6 +202,7 @@ export function useRecyclerViewController<T>(
|
|
|
259
202
|
animated,
|
|
260
203
|
skipFirstItemOffset = true,
|
|
261
204
|
}: ScrollToOffsetParams) => {
|
|
205
|
+
const { horizontal } = recyclerViewManager.props;
|
|
262
206
|
if (scrollViewRef.current) {
|
|
263
207
|
// Adjust offset for RTL layouts in horizontal mode
|
|
264
208
|
if (I18nManager.isRTL && horizontal) {
|
|
@@ -287,6 +231,9 @@ export function useRecyclerViewController<T>(
|
|
|
287
231
|
});
|
|
288
232
|
}
|
|
289
233
|
},
|
|
234
|
+
clearLayoutCacheOnUpdate: () => {
|
|
235
|
+
recyclerViewManager.markLayoutManagerDirty();
|
|
236
|
+
},
|
|
290
237
|
|
|
291
238
|
// Expose native scroll view methods
|
|
292
239
|
flashScrollIndicators: () => {
|
|
@@ -306,13 +253,16 @@ export function useRecyclerViewController<T>(
|
|
|
306
253
|
* Scrolls to the end of the list.
|
|
307
254
|
*/
|
|
308
255
|
scrollToEnd: async ({ animated }: ScrollToEdgeParams = {}) => {
|
|
256
|
+
const { data } = recyclerViewManager.props;
|
|
309
257
|
if (data && data.length > 0) {
|
|
310
258
|
await handlerMethods.scrollToIndex({
|
|
311
259
|
index: data.length - 1,
|
|
312
260
|
animated,
|
|
313
261
|
});
|
|
314
262
|
}
|
|
315
|
-
|
|
263
|
+
setTimeout(() => {
|
|
264
|
+
scrollViewRef.current!.scrollToEnd({ animated });
|
|
265
|
+
}, 0);
|
|
316
266
|
},
|
|
317
267
|
|
|
318
268
|
/**
|
|
@@ -335,8 +285,15 @@ export function useRecyclerViewController<T>(
|
|
|
335
285
|
viewPosition,
|
|
336
286
|
viewOffset,
|
|
337
287
|
}: ScrollToIndexParams) => {
|
|
338
|
-
|
|
339
|
-
|
|
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);
|
|
340
297
|
|
|
341
298
|
const getFinalOffset = () => {
|
|
342
299
|
const layout = recyclerViewManager.getLayout(index);
|
|
@@ -360,74 +317,104 @@ export function useRecyclerViewController<T>(
|
|
|
360
317
|
finalOffset += viewOffset;
|
|
361
318
|
}
|
|
362
319
|
}
|
|
363
|
-
return finalOffset;
|
|
320
|
+
return finalOffset + recyclerViewManager.firstItemOffset;
|
|
364
321
|
};
|
|
365
|
-
|
|
366
|
-
|
|
322
|
+
const lastAbsoluteScrollOffset =
|
|
323
|
+
recyclerViewManager.getAbsoluteLastScrollOffset();
|
|
367
324
|
const bufferForScroll = horizontal
|
|
368
325
|
? recyclerViewManager.getWindowSize().width
|
|
369
326
|
: recyclerViewManager.getWindowSize().height;
|
|
370
327
|
|
|
371
328
|
const bufferForCompute = bufferForScroll * 2;
|
|
372
329
|
|
|
373
|
-
|
|
374
|
-
lastScrollOffset =
|
|
375
|
-
|
|
376
|
-
lastScrollOffset
|
|
377
|
-
);
|
|
378
|
-
recyclerViewManager.setScrollDirection("forward");
|
|
379
|
-
} else {
|
|
380
|
-
lastScrollOffset = Math.min(
|
|
381
|
-
finalOffset + bufferForCompute,
|
|
382
|
-
lastScrollOffset
|
|
383
|
-
);
|
|
384
|
-
recyclerViewManager.setScrollDirection("backward");
|
|
385
|
-
}
|
|
330
|
+
const getStartScrollOffset = () => {
|
|
331
|
+
let lastScrollOffset = lastAbsoluteScrollOffset;
|
|
332
|
+
const finalOffset = getFinalOffset();
|
|
386
333
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
await updateScrollOffsetAsync(
|
|
394
|
-
finalOffset + (lastScrollOffset - finalOffset) * (i / 4)
|
|
334
|
+
if (finalOffset > lastScrollOffset) {
|
|
335
|
+
lastScrollOffset = Math.max(
|
|
336
|
+
finalOffset - bufferForCompute,
|
|
337
|
+
lastScrollOffset
|
|
395
338
|
);
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
await updateScrollOffsetAsync(
|
|
404
|
-
lastScrollOffset + (finalOffset - lastScrollOffset) * (i / 4)
|
|
339
|
+
recyclerViewManager.setScrollDirection("forward");
|
|
340
|
+
} else {
|
|
341
|
+
lastScrollOffset = Math.min(
|
|
342
|
+
finalOffset + bufferForCompute,
|
|
343
|
+
lastScrollOffset
|
|
405
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
|
+
|
|
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
|
|
406
387
|
}
|
|
407
388
|
}
|
|
389
|
+
|
|
408
390
|
finalOffset = getFinalOffset();
|
|
409
391
|
const maxOffset = recyclerViewManager.getMaxScrollOffset();
|
|
410
392
|
|
|
411
393
|
if (finalOffset > maxOffset) {
|
|
412
394
|
finalOffset = maxOffset;
|
|
413
395
|
}
|
|
396
|
+
|
|
414
397
|
if (animated) {
|
|
415
|
-
// We don't need to add firstItemOffset here as it
|
|
398
|
+
// We don't need to add firstItemOffset here as it's already added
|
|
416
399
|
handlerMethods.scrollToOffset({
|
|
417
|
-
offset:
|
|
400
|
+
offset: startScrollOffset,
|
|
418
401
|
animated: false,
|
|
419
|
-
skipFirstItemOffset:
|
|
402
|
+
skipFirstItemOffset: true,
|
|
420
403
|
});
|
|
421
404
|
}
|
|
422
405
|
handlerMethods.scrollToOffset({
|
|
423
406
|
offset: finalOffset,
|
|
424
407
|
animated,
|
|
425
|
-
skipFirstItemOffset:
|
|
408
|
+
skipFirstItemOffset: true,
|
|
426
409
|
});
|
|
427
410
|
|
|
428
|
-
setTimeout(
|
|
429
|
-
|
|
430
|
-
|
|
411
|
+
setTimeout(
|
|
412
|
+
() => {
|
|
413
|
+
pauseOffsetCorrection.current = false;
|
|
414
|
+
recyclerViewManager.setOffsetProjectionEnabled(true);
|
|
415
|
+
},
|
|
416
|
+
animated ? 300 : 200
|
|
417
|
+
);
|
|
431
418
|
}
|
|
432
419
|
},
|
|
433
420
|
|
|
@@ -441,11 +428,10 @@ export function useRecyclerViewController<T>(
|
|
|
441
428
|
viewPosition,
|
|
442
429
|
viewOffset,
|
|
443
430
|
}: ScrollToItemParams<T>) => {
|
|
431
|
+
const { data } = recyclerViewManager.props;
|
|
444
432
|
if (scrollViewRef.current && data) {
|
|
445
433
|
// Find the index of the item in the data array
|
|
446
|
-
const index =
|
|
447
|
-
(dataItem) => dataItem === item
|
|
448
|
-
);
|
|
434
|
+
const index = data.findIndex((dataItem) => dataItem === item);
|
|
449
435
|
if (index >= 0) {
|
|
450
436
|
handlerMethods.scrollToIndex({
|
|
451
437
|
index,
|
|
@@ -465,7 +451,7 @@ export function useRecyclerViewController<T>(
|
|
|
465
451
|
return recyclerViewManager.getWindowSize();
|
|
466
452
|
},
|
|
467
453
|
getLayout: (index: number) => {
|
|
468
|
-
return recyclerViewManager.
|
|
454
|
+
return recyclerViewManager.tryGetLayout(index);
|
|
469
455
|
},
|
|
470
456
|
getAbsoluteLastScrollOffset: () => {
|
|
471
457
|
return recyclerViewManager.getAbsoluteLastScrollOffset();
|
|
@@ -476,11 +462,11 @@ export function useRecyclerViewController<T>(
|
|
|
476
462
|
recordInteraction: () => {
|
|
477
463
|
recyclerViewManager.recordInteraction();
|
|
478
464
|
},
|
|
479
|
-
|
|
480
|
-
return recyclerViewManager.
|
|
465
|
+
computeVisibleIndices: () => {
|
|
466
|
+
return recyclerViewManager.computeVisibleIndices();
|
|
481
467
|
},
|
|
482
468
|
getFirstVisibleIndex: () => {
|
|
483
|
-
return recyclerViewManager.
|
|
469
|
+
return recyclerViewManager.computeVisibleIndices().startIndex;
|
|
484
470
|
},
|
|
485
471
|
recomputeViewableItems: () => {
|
|
486
472
|
recyclerViewManager.recomputeViewableItems();
|
|
@@ -489,10 +475,55 @@ export function useRecyclerViewController<T>(
|
|
|
489
475
|
* Disables item recycling in preparation for layout animations.
|
|
490
476
|
*/
|
|
491
477
|
prepareForLayoutAnimationRender: () => {
|
|
492
|
-
recyclerViewManager.disableRecycling
|
|
478
|
+
recyclerViewManager.disableRecycling(true);
|
|
493
479
|
},
|
|
494
480
|
};
|
|
495
|
-
}, [
|
|
481
|
+
}, [
|
|
482
|
+
recyclerViewManager,
|
|
483
|
+
scrollViewRef,
|
|
484
|
+
setTimeout,
|
|
485
|
+
isUnmounted,
|
|
486
|
+
updateScrollOffsetAsync,
|
|
487
|
+
]);
|
|
488
|
+
|
|
489
|
+
const applyInitialScrollIndex = useCallback(() => {
|
|
490
|
+
const { horizontal, data } = recyclerViewManager.props;
|
|
491
|
+
|
|
492
|
+
const initialScrollIndex =
|
|
493
|
+
recyclerViewManager.getInitialScrollIndex() ?? -1;
|
|
494
|
+
const dataLength = data?.length ?? 0;
|
|
495
|
+
if (
|
|
496
|
+
initialScrollIndex >= 0 &&
|
|
497
|
+
initialScrollIndex < dataLength &&
|
|
498
|
+
!initialScrollCompletedRef.current &&
|
|
499
|
+
recyclerViewManager.getIsFirstLayoutComplete()
|
|
500
|
+
) {
|
|
501
|
+
// Use setTimeout to ensure that we keep trying to scroll on first few renders
|
|
502
|
+
setTimeout(() => {
|
|
503
|
+
initialScrollCompletedRef.current = true;
|
|
504
|
+
pauseOffsetCorrection.current = false;
|
|
505
|
+
}, 100);
|
|
506
|
+
|
|
507
|
+
pauseOffsetCorrection.current = true;
|
|
508
|
+
|
|
509
|
+
const offset = horizontal
|
|
510
|
+
? recyclerViewManager.getLayout(initialScrollIndex).x
|
|
511
|
+
: recyclerViewManager.getLayout(initialScrollIndex).y;
|
|
512
|
+
handlerMethods.scrollToOffset({
|
|
513
|
+
offset,
|
|
514
|
+
animated: false,
|
|
515
|
+
skipFirstItemOffset: false,
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
setTimeout(() => {
|
|
519
|
+
handlerMethods.scrollToOffset({
|
|
520
|
+
offset,
|
|
521
|
+
animated: false,
|
|
522
|
+
skipFirstItemOffset: false,
|
|
523
|
+
});
|
|
524
|
+
}, 0);
|
|
525
|
+
}
|
|
526
|
+
}, [handlerMethods, recyclerViewManager, setTimeout]);
|
|
496
527
|
|
|
497
528
|
// Expose imperative methods through the ref
|
|
498
529
|
useImperativeHandle(
|
|
@@ -500,8 +531,8 @@ export function useRecyclerViewController<T>(
|
|
|
500
531
|
() => {
|
|
501
532
|
return { ...scrollViewRef.current, ...handlerMethods };
|
|
502
533
|
},
|
|
503
|
-
[handlerMethods]
|
|
534
|
+
[handlerMethods, scrollViewRef]
|
|
504
535
|
);
|
|
505
536
|
|
|
506
|
-
return { applyContentOffset, applyInitialScrollIndex };
|
|
537
|
+
return { applyContentOffset, applyInitialScrollIndex, handlerMethods };
|
|
507
538
|
}
|
|
@@ -2,15 +2,20 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
2
2
|
|
|
3
3
|
import { RecyclerViewProps } from "../RecyclerViewProps";
|
|
4
4
|
import { RecyclerViewManager } from "../RecyclerViewManager";
|
|
5
|
+
import { VelocityTracker } from "../helpers/VelocityTracker";
|
|
5
6
|
|
|
6
7
|
export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
|
|
7
8
|
const [recyclerViewManager] = useState<RecyclerViewManager<T>>(
|
|
8
9
|
() => new RecyclerViewManager(props)
|
|
9
10
|
);
|
|
11
|
+
const [velocityTracker] = useState(() => new VelocityTracker());
|
|
12
|
+
|
|
10
13
|
const { data } = props;
|
|
11
14
|
|
|
12
15
|
useMemo(() => {
|
|
13
16
|
recyclerViewManager.updateProps(props);
|
|
17
|
+
// used to update props so rule can be disabled
|
|
18
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14
19
|
}, [props]);
|
|
15
20
|
|
|
16
21
|
/**
|
|
@@ -18,13 +23,18 @@ export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
|
|
|
18
23
|
*/
|
|
19
24
|
useMemo(() => {
|
|
20
25
|
recyclerViewManager.processDataUpdate();
|
|
26
|
+
// used to process data update so rule can be disabled
|
|
27
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21
28
|
}, [data]);
|
|
22
29
|
|
|
23
30
|
useEffect(() => {
|
|
24
31
|
return () => {
|
|
25
32
|
recyclerViewManager.dispose();
|
|
33
|
+
velocityTracker.cleanUp();
|
|
26
34
|
};
|
|
35
|
+
// needs to run only on unmount
|
|
36
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
27
37
|
}, []);
|
|
28
38
|
|
|
29
|
-
return { recyclerViewManager };
|
|
39
|
+
return { recyclerViewManager, velocityTracker };
|
|
30
40
|
};
|
|
@@ -105,7 +105,7 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
|
|
|
105
105
|
const ForwardedScrollComponent = React.forwardRef((_props, ref) =>
|
|
106
106
|
(renderScrollComponent as any)({ ..._props, ref } as any)
|
|
107
107
|
);
|
|
108
|
-
ForwardedScrollComponent.displayName = "
|
|
108
|
+
ForwardedScrollComponent.displayName = "CustomScrollView";
|
|
109
109
|
scrollComponent = ForwardedScrollComponent as any;
|
|
110
110
|
} else if (renderScrollComponent) {
|
|
111
111
|
scrollComponent = renderScrollComponent;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Hook that provides
|
|
5
|
-
* Any timeouts created with
|
|
4
|
+
* Hook that provides a setTimeout which is aware of component unmount state.
|
|
5
|
+
* Any timeouts created with this hook will be automatically cleared when the component unmounts.
|
|
6
6
|
*/
|
|
7
|
-
export function
|
|
7
|
+
export function useUnmountAwareTimeout() {
|
|
8
8
|
// Store active timeout IDs in a Set for more efficient add/remove operations
|
|
9
9
|
const [timeoutIds] = useState<Set<NodeJS.Timeout>>(() => new Set());
|
|
10
10
|
|
|
@@ -35,3 +35,39 @@ export function useUnmountAwareCallbacks() {
|
|
|
35
35
|
setTimeout,
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hook that provides a requestAnimationFrame which is aware of component unmount state.
|
|
41
|
+
* Any animation frames requested with this hook will be automatically canceled when the component unmounts.
|
|
42
|
+
*/
|
|
43
|
+
export function useUnmountAwareAnimationFrame() {
|
|
44
|
+
// Store active animation frame request IDs in a Set for more efficient add/remove operations
|
|
45
|
+
const [requestIds] = useState<Set<number>>(() => new Set());
|
|
46
|
+
|
|
47
|
+
// Cancel all animation frame requests on unmount
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
return () => {
|
|
50
|
+
requestIds.forEach((id) => cancelAnimationFrame(id));
|
|
51
|
+
requestIds.clear();
|
|
52
|
+
};
|
|
53
|
+
}, [requestIds]);
|
|
54
|
+
|
|
55
|
+
// Create a safe requestAnimationFrame that will be canceled on unmount
|
|
56
|
+
const requestAnimationFrame = useCallback(
|
|
57
|
+
(callback: FrameRequestCallback): void => {
|
|
58
|
+
const id = global.requestAnimationFrame((timestamp) => {
|
|
59
|
+
// Remove this request ID from the tracking set
|
|
60
|
+
requestIds.delete(id);
|
|
61
|
+
callback(timestamp);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Track this request ID
|
|
65
|
+
requestIds.add(id);
|
|
66
|
+
},
|
|
67
|
+
[requestIds]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
requestAnimationFrame,
|
|
72
|
+
};
|
|
73
|
+
}
|