react-native-reanimated-carousel 5.1.0 → 5.2.0

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 (122) hide show
  1. package/README.md +6 -0
  2. package/lib/commonjs/components/CarouselLayout.js +1 -1
  3. package/lib/commonjs/components/CarouselLayout.js.map +1 -1
  4. package/lib/commonjs/components/ItemLayout.js +1 -1
  5. package/lib/commonjs/components/ItemLayout.js.map +1 -1
  6. package/lib/commonjs/components/ItemRenderer.js +1 -1
  7. package/lib/commonjs/components/ItemRenderer.js.map +1 -1
  8. package/lib/commonjs/components/Pagination/PaginationItem.js +1 -1
  9. package/lib/commonjs/components/Pagination/PaginationItem.js.map +1 -1
  10. package/lib/commonjs/components/ScrollViewGesture.js +1 -1
  11. package/lib/commonjs/components/ScrollViewGesture.js.map +1 -1
  12. package/lib/commonjs/hooks/useCarouselController.js +1 -1
  13. package/lib/commonjs/hooks/useCarouselController.js.map +1 -1
  14. package/lib/commonjs/hooks/useCommonVariables.js +1 -1
  15. package/lib/commonjs/hooks/useCommonVariables.js.map +1 -1
  16. package/lib/commonjs/hooks/useInitProps.js +1 -1
  17. package/lib/commonjs/hooks/useInitProps.js.map +1 -1
  18. package/lib/commonjs/hooks/useOffsetX.js +1 -1
  19. package/lib/commonjs/hooks/useOffsetX.js.map +1 -1
  20. package/lib/commonjs/hooks/useOnProgressChange.js +1 -1
  21. package/lib/commonjs/hooks/useOnProgressChange.js.map +1 -1
  22. package/lib/commonjs/hooks/useVisibleRanges.js +1 -1
  23. package/lib/commonjs/hooks/useVisibleRanges.js.map +1 -1
  24. package/lib/commonjs/layouts/normal.js +1 -1
  25. package/lib/commonjs/layouts/normal.js.map +1 -1
  26. package/lib/commonjs/layouts/parallax.js +1 -1
  27. package/lib/commonjs/layouts/parallax.js.map +1 -1
  28. package/lib/commonjs/store/index.js.map +1 -1
  29. package/lib/commonjs/utils/web-only-dependencies.js +2 -0
  30. package/lib/commonjs/utils/web-only-dependencies.js.map +1 -0
  31. package/lib/module/components/CarouselLayout.js +1 -1
  32. package/lib/module/components/CarouselLayout.js.map +1 -1
  33. package/lib/module/components/ItemLayout.js +1 -1
  34. package/lib/module/components/ItemLayout.js.map +1 -1
  35. package/lib/module/components/ItemRenderer.js +1 -1
  36. package/lib/module/components/ItemRenderer.js.map +1 -1
  37. package/lib/module/components/Pagination/PaginationItem.js +1 -1
  38. package/lib/module/components/Pagination/PaginationItem.js.map +1 -1
  39. package/lib/module/components/ScrollViewGesture.js +1 -1
  40. package/lib/module/components/ScrollViewGesture.js.map +1 -1
  41. package/lib/module/hooks/useCarouselController.js +1 -1
  42. package/lib/module/hooks/useCarouselController.js.map +1 -1
  43. package/lib/module/hooks/useCommonVariables.js +1 -1
  44. package/lib/module/hooks/useCommonVariables.js.map +1 -1
  45. package/lib/module/hooks/useInitProps.js +1 -1
  46. package/lib/module/hooks/useInitProps.js.map +1 -1
  47. package/lib/module/hooks/useOffsetX.js +1 -1
  48. package/lib/module/hooks/useOffsetX.js.map +1 -1
  49. package/lib/module/hooks/useOnProgressChange.js +1 -1
  50. package/lib/module/hooks/useOnProgressChange.js.map +1 -1
  51. package/lib/module/hooks/useVisibleRanges.js +1 -1
  52. package/lib/module/hooks/useVisibleRanges.js.map +1 -1
  53. package/lib/module/layouts/normal.js +1 -1
  54. package/lib/module/layouts/normal.js.map +1 -1
  55. package/lib/module/layouts/parallax.js +1 -1
  56. package/lib/module/layouts/parallax.js.map +1 -1
  57. package/lib/module/store/index.js.map +1 -1
  58. package/lib/module/utils/web-only-dependencies.js +2 -0
  59. package/lib/module/utils/web-only-dependencies.js.map +1 -0
  60. package/lib/typescript/commonjs/components/CarouselLayout.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/components/ItemLayout.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/components/ItemRenderer.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/components/Pagination/PaginationItem.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/components/ScrollViewGesture.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/hooks/useCarouselController.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/hooks/useCommonVariables.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/hooks/useInitProps.d.ts +2 -1
  68. package/lib/typescript/commonjs/hooks/useInitProps.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/hooks/useOffsetX.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/hooks/useOnProgressChange.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/hooks/useVisibleRanges.d.ts +1 -0
  72. package/lib/typescript/commonjs/hooks/useVisibleRanges.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/layouts/normal.d.ts +2 -9
  74. package/lib/typescript/commonjs/layouts/normal.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/layouts/parallax.d.ts +2 -16
  76. package/lib/typescript/commonjs/layouts/parallax.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/public-types.d.ts +1 -0
  78. package/lib/typescript/commonjs/public-types.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/store/index.d.ts +1 -1
  80. package/lib/typescript/commonjs/store/index.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/utils/web-only-dependencies.d.ts +7 -0
  82. package/lib/typescript/commonjs/utils/web-only-dependencies.d.ts.map +1 -0
  83. package/lib/typescript/module/components/CarouselLayout.d.ts.map +1 -1
  84. package/lib/typescript/module/components/ItemLayout.d.ts.map +1 -1
  85. package/lib/typescript/module/components/ItemRenderer.d.ts.map +1 -1
  86. package/lib/typescript/module/components/Pagination/PaginationItem.d.ts.map +1 -1
  87. package/lib/typescript/module/components/ScrollViewGesture.d.ts.map +1 -1
  88. package/lib/typescript/module/hooks/useCarouselController.d.ts.map +1 -1
  89. package/lib/typescript/module/hooks/useCommonVariables.d.ts.map +1 -1
  90. package/lib/typescript/module/hooks/useInitProps.d.ts +2 -1
  91. package/lib/typescript/module/hooks/useInitProps.d.ts.map +1 -1
  92. package/lib/typescript/module/hooks/useOffsetX.d.ts.map +1 -1
  93. package/lib/typescript/module/hooks/useOnProgressChange.d.ts.map +1 -1
  94. package/lib/typescript/module/hooks/useVisibleRanges.d.ts +1 -0
  95. package/lib/typescript/module/hooks/useVisibleRanges.d.ts.map +1 -1
  96. package/lib/typescript/module/layouts/normal.d.ts +2 -9
  97. package/lib/typescript/module/layouts/normal.d.ts.map +1 -1
  98. package/lib/typescript/module/layouts/parallax.d.ts +2 -16
  99. package/lib/typescript/module/layouts/parallax.d.ts.map +1 -1
  100. package/lib/typescript/module/public-types.d.ts +1 -0
  101. package/lib/typescript/module/public-types.d.ts.map +1 -1
  102. package/lib/typescript/module/store/index.d.ts +1 -1
  103. package/lib/typescript/module/store/index.d.ts.map +1 -1
  104. package/lib/typescript/module/utils/web-only-dependencies.d.ts +7 -0
  105. package/lib/typescript/module/utils/web-only-dependencies.d.ts.map +1 -0
  106. package/package.json +3 -2
  107. package/src/components/CarouselLayout.tsx +15 -4
  108. package/src/components/ItemLayout.tsx +14 -4
  109. package/src/components/ItemRenderer.tsx +36 -4
  110. package/src/components/Pagination/PaginationItem.tsx +22 -20
  111. package/src/components/ScrollViewGesture.tsx +12 -5
  112. package/src/hooks/useCarouselController.tsx +3 -2
  113. package/src/hooks/useCommonVariables.ts +3 -2
  114. package/src/hooks/useInitProps.ts +13 -0
  115. package/src/hooks/useOffsetX.ts +11 -1
  116. package/src/hooks/useOnProgressChange.ts +2 -1
  117. package/src/hooks/useVisibleRanges.tsx +5 -2
  118. package/src/layouts/normal.ts +2 -1
  119. package/src/layouts/parallax.ts +2 -1
  120. package/src/public-types.ts +1 -0
  121. package/src/store/index.tsx +1 -1
  122. package/src/utils/web-only-dependencies.ts +10 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-reanimated-carousel",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "packageManager": "yarn@4.0.2",
5
5
  "description": "A performant, customizable carousel for React Native powered by Reanimated.",
6
6
  "author": "Doho <zhaodonghao586@outlook.com> (https://github.com/dohooo)",
@@ -54,8 +54,9 @@
54
54
  "test:watch": "jest --watch",
55
55
  "test:coverage": "jest --coverage",
56
56
  "test:ci": "jest --coverage --ci --runInBand --reporters=default --reporters=jest-junit",
57
- "types": "tsc --noEmit && yarn types:public",
57
+ "types": "tsc --noEmit && yarn types:public && yarn types:non-strict",
58
58
  "types:public": "tsc -p tsconfig.type-tests.json",
59
+ "types:non-strict": "tsc -p tsconfig.non-strict.json",
59
60
  "lint": "biome check .",
60
61
  "lint:fix": "biome check --write .",
61
62
  "format": "biome format .",
@@ -8,6 +8,7 @@ import { useLayoutConfig } from "../hooks/useLayoutConfig";
8
8
  import { useOnProgressChange } from "../hooks/useOnProgressChange";
9
9
  import { useGlobalState } from "../store";
10
10
  import type { CarouselRef } from "../types";
11
+ import { webOnlyDependencies } from "../utils/web-only-dependencies";
11
12
  import { ItemRenderer } from "./ItemRenderer";
12
13
  import { ScrollViewGesture } from "./ScrollViewGesture";
13
14
 
@@ -66,11 +67,13 @@ export const CarouselLayout = React.forwardRef<CarouselRef>((_props, ref) => {
66
67
  const { size, handlerOffset, resolvedSize, sizePhase, sizeExplicit } = common;
67
68
  const layoutConfig = useLayoutConfig({ ...props, size });
68
69
 
70
+ const isSizeReadyDependencies = webOnlyDependencies([resolvedSize, sizePhase]);
69
71
  const isSizeReady = useDerivedValue(() => {
70
72
  const currentSize = resolvedSize.value ?? 0;
71
73
  return sizePhase.value === "ready" && currentSize > 0;
72
- }, [resolvedSize, sizePhase]);
74
+ }, isSizeReadyDependencies);
73
75
 
76
+ const offsetXDependencies = webOnlyDependencies([loop, dataLength, handlerOffset, resolvedSize]);
74
77
  const offsetX = useDerivedValue(() => {
75
78
  const currentSize = resolvedSize.value ?? 0;
76
79
  if (currentSize <= 0) return 0;
@@ -83,7 +86,7 @@ export const CarouselLayout = React.forwardRef<CarouselRef>((_props, ref) => {
83
86
 
84
87
  const x = value % totalSize;
85
88
  return Number.isNaN(x) ? 0 : x;
86
- }, [loop, dataLength, handlerOffset, resolvedSize]);
89
+ }, offsetXDependencies);
87
90
 
88
91
  useOnProgressChange({
89
92
  loop,
@@ -135,7 +138,7 @@ export const CarouselLayout = React.forwardRef<CarouselRef>((_props, ref) => {
135
138
  if (state.ready) scheduleOnRN(triggerAutoPlay);
136
139
  else scheduleOnRN(pauseAutoPlay);
137
140
  },
138
- [autoplay]
141
+ webOnlyDependencies([autoplay])
139
142
  );
140
143
 
141
144
  const scrollViewGestureOnScrollStart = React.useCallback(() => {
@@ -158,6 +161,14 @@ export const CarouselLayout = React.forwardRef<CarouselRef>((_props, ref) => {
158
161
  StyleSheet.flatten(contentContainerStyle as StyleProp<ViewStyle>) || {};
159
162
  const flattenedStyle = StyleSheet.flatten(style) || {};
160
163
 
164
+ const layoutStyleDependencies = webOnlyDependencies([
165
+ flattenedStyle,
166
+ isSizeReady,
167
+ isVertical,
168
+ resolvedSize,
169
+ sizePhase,
170
+ sizeExplicit,
171
+ ]);
161
172
  const layoutStyle = useAnimatedStyle(() => {
162
173
  const measuredSize = resolvedSize.value ?? 0;
163
174
  const { width, height } = resolveCarouselLayoutStyle({
@@ -172,7 +183,7 @@ export const CarouselLayout = React.forwardRef<CarouselRef>((_props, ref) => {
172
183
  height,
173
184
  opacity: isSizeReady.value ? 1 : 0,
174
185
  };
175
- }, [flattenedStyle, isSizeReady, isVertical, resolvedSize, sizePhase, sizeExplicit]);
186
+ }, layoutStyleDependencies);
176
187
 
177
188
  return (
178
189
  <View testID={testID} style={[styles.layoutContainer, style]} onLayout={props.onLayout}>
@@ -18,6 +18,7 @@ import type { CarouselItemAnimation } from "../types";
18
18
  import { getHorizontalStackOffsetType } from "../utils/carousel-direction";
19
19
  import { positiveModulo } from "../utils/carousel-math";
20
20
  import { sanitizeAnimationStyle } from "../utils/sanitize-animation-style";
21
+ import { webOnlyDependencies } from "../utils/web-only-dependencies";
21
22
 
22
23
  export type ItemAnimationStyle = CarouselItemAnimation;
23
24
 
@@ -67,6 +68,13 @@ export const ItemLayout: React.FC<{
67
68
  const size = (itemSize ?? fallbackSize) || 0;
68
69
  const effectivePageSize = size > 0 ? size : undefined;
69
70
 
71
+ const dimensionsStyleDependencies = webOnlyDependencies([
72
+ effectivePageSize,
73
+ isVertical,
74
+ itemSize,
75
+ styleHeightNumber,
76
+ styleWidthNumber,
77
+ ]);
70
78
  const dimensionsStyle = useAnimatedStyle<ViewStyle>(() => {
71
79
  const widthCandidate = isVertical ? styleWidthNumber : itemSize;
72
80
  const heightCandidate = isVertical ? itemSize : styleHeightNumber;
@@ -95,7 +103,7 @@ export const ItemLayout: React.FC<{
95
103
  width: computedWidth,
96
104
  height: computedHeight,
97
105
  };
98
- }, [effectivePageSize, isVertical, itemSize, styleHeightNumber, styleWidthNumber]);
106
+ }, dimensionsStyleDependencies);
99
107
 
100
108
  let offsetXConfig: OffsetOptions = {
101
109
  handlerOffset,
@@ -137,17 +145,19 @@ export const ItemLayout: React.FC<{
137
145
  scheduleOnRN(setIsAccessibilityCurrent, isCurrent);
138
146
  }
139
147
  },
140
- [isCurrentForAccessibility]
148
+ webOnlyDependencies([isCurrentForAccessibility])
141
149
  );
142
150
 
151
+ const relativeProgressDependencies = webOnlyDependencies([x, size]);
143
152
  const relativeProgress = useDerivedValue(() => {
144
153
  if (!size) return 0;
145
154
  return x.value / size;
146
- }, [x, size]);
155
+ }, relativeProgressDependencies);
156
+ const animatedStyleDependencies = webOnlyDependencies([animationStyle, rawIndex, x, size]);
147
157
  const animatedStyle = useAnimatedStyle<ViewStyle>(() => {
148
158
  const safeSize = size || 1;
149
159
  return sanitizeAnimationStyle(animationStyle(x.value / safeSize, rawIndex));
150
- }, [animationStyle, rawIndex, x, size]);
160
+ }, animatedStyleDependencies);
151
161
 
152
162
  // TODO: For dynamic dimension in the future
153
163
  // function handleLayout(e: LayoutChangeEvent) {
@@ -8,9 +8,14 @@ import type { ItemAnimationStyle } from "./ItemLayout";
8
8
  import { ItemLayout } from "./ItemLayout";
9
9
 
10
10
  import type { VisibleRanges } from "../hooks/useVisibleRanges";
11
- import { computeVisibleRanges, useVisibleRanges } from "../hooks/useVisibleRanges";
11
+ import {
12
+ computeVisibleRanges,
13
+ normalizeWindowSize,
14
+ useVisibleRanges,
15
+ } from "../hooks/useVisibleRanges";
12
16
  import type { CarouselItemAnimation, CarouselRenderItem } from "../types";
13
17
  import { computedRealIndexWithAutoFillData } from "../utils/computed-with-auto-fill-data";
18
+ import { webOnlyDependencies } from "../utils/web-only-dependencies";
14
19
 
15
20
  interface Props {
16
21
  data: unknown[];
@@ -71,12 +76,34 @@ export const ItemRenderer: FC<Props> = (props) => {
71
76
 
72
77
  const [displayedItems, setDisplayedItems] = React.useState<VisibleRanges>(initialRanges);
73
78
 
79
+ // When the window covers every item, the ranges always span the whole list. Skip them so a
80
+ // stale range from before a data change cannot unmount keyed items for one render.
81
+ const rendersAllItems = normalizeWindowSize(dataLength, renderWindowSize) >= dataLength;
82
+
74
83
  useAnimatedReaction(
75
84
  () => visibleRanges.value,
76
85
  (ranges) => scheduleOnRN(setDisplayedItems, ranges),
77
- [visibleRanges]
86
+ webOnlyDependencies([visibleRanges])
78
87
  );
79
88
 
89
+ // With a render window, the ranges still describe the previous data for the renders between a
90
+ // data change and the next range update. Keep the keyed items mounted with the last up-to-date
91
+ // ranges until then, so an item that shifted index is not unmounted and mounted again.
92
+ const lastSettledRender = React.useRef<{
93
+ data: unknown[];
94
+ ranges: VisibleRanges;
95
+ keys: ReadonlySet<string>;
96
+ } | null>(null);
97
+ const settled = lastSettledRender.current;
98
+ const rangesAreStale =
99
+ !!keyExtractor && !!settled && settled.ranges === displayedItems && settled.data !== data;
100
+ const renderedKeys = new Set<string>();
101
+
102
+ React.useEffect(() => {
103
+ if (rangesAreStale) return;
104
+ lastSettledRender.current = { data, ranges: displayedItems, keys: renderedKeys };
105
+ });
106
+
80
107
  return (
81
108
  <>
82
109
  {data.map((item, index) => {
@@ -89,11 +116,12 @@ export const ItemRenderer: FC<Props> = (props) => {
89
116
 
90
117
  const { negativeRange, positiveRange } = displayedItems;
91
118
 
92
- const shouldRender =
119
+ const isInRange =
120
+ rendersAllItems ||
93
121
  (index >= negativeRange[0] && index <= negativeRange[1]) ||
94
122
  (index >= positiveRange[0] && index <= positiveRange[1]);
95
123
 
96
- if (!shouldRender) return null;
124
+ if (!isInRange && !rangesAreStale) return null;
97
125
 
98
126
  const rawItem = rawData[realIndex] ?? item;
99
127
  const key = keyExtractor
@@ -102,6 +130,10 @@ export const ItemRenderer: FC<Props> = (props) => {
102
130
  )}`
103
131
  : String(index);
104
132
 
133
+ if (!isInRange && !settled?.keys.has(key)) return null;
134
+
135
+ renderedKeys.add(key);
136
+
105
137
  return (
106
138
  <ItemLayout
107
139
  key={key}
@@ -12,6 +12,7 @@ import { scheduleOnRN } from "react-native-worklets";
12
12
 
13
13
  import type { PaginationDotStyle, PaginationProps } from "../../public-types";
14
14
  import { getNearestLoopPosition, getSettledRawIndex } from "../../utils/carousel-math";
15
+ import { webOnlyDependencies } from "../../utils/web-only-dependencies";
15
16
 
16
17
  const DEFAULT_DOT_SIZE = 10;
17
18
  const DEFAULT_DOT_COLOR = "#D1D5DB";
@@ -86,9 +87,28 @@ export function PaginationItem(props: {
86
87
  scheduleOnRN(setSelected, nextSelected);
87
88
  }
88
89
  },
89
- [count, index, progress]
90
+ webOnlyDependencies([count, index, progress])
90
91
  );
91
92
 
93
+ const animatedStyleDependencies = webOnlyDependencies([
94
+ activeBackgroundColor,
95
+ activeBorderColor,
96
+ activeBorderRadius,
97
+ activeBorderWidth,
98
+ activeHeight,
99
+ activeOpacity,
100
+ activeWidth,
101
+ baseBackgroundColor,
102
+ baseBorderColor,
103
+ baseBorderRadius,
104
+ baseBorderWidth,
105
+ baseHeight,
106
+ baseOpacity,
107
+ baseWidth,
108
+ count,
109
+ index,
110
+ progress,
111
+ ]);
92
112
  const animatedStyle = useAnimatedStyle<ViewStyle>(() => {
93
113
  const distance = Math.min(1, getPaginationDotDistance(progress.value, index, count));
94
114
 
@@ -111,25 +131,7 @@ export function PaginationItem(props: {
111
131
  backgroundColor: interpolateDotColor(distance, activeBackgroundColor, baseBackgroundColor),
112
132
  opacity: interpolate(distance, [0, 1], [activeOpacity, baseOpacity], Extrapolation.CLAMP),
113
133
  };
114
- }, [
115
- activeBackgroundColor,
116
- activeBorderColor,
117
- activeBorderRadius,
118
- activeBorderWidth,
119
- activeHeight,
120
- activeOpacity,
121
- activeWidth,
122
- baseBackgroundColor,
123
- baseBorderColor,
124
- baseBorderRadius,
125
- baseBorderWidth,
126
- baseHeight,
127
- baseOpacity,
128
- baseWidth,
129
- count,
130
- index,
131
- progress,
132
- ]);
134
+ }, animatedStyleDependencies);
133
135
 
134
136
  const reservedStyle: ViewStyle = {
135
137
  alignItems: "center",
@@ -21,6 +21,7 @@ import { useGlobalState } from "../store";
21
21
  import { toLogicalGestureValue } from "../utils/carousel-direction";
22
22
  import { computeGestureTranslation } from "../utils/compute-gesture-translation";
23
23
  import { dealWithAnimation } from "../utils/deal-with-animation";
24
+ import { webOnlyDependencies } from "../utils/web-only-dependencies";
24
25
 
25
26
  interface Props {
26
27
  size: number;
@@ -40,6 +41,7 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
40
41
  onConfigurePanGesture,
41
42
  orientation,
42
43
  snapMode,
44
+ snapMomentum,
43
45
  loop,
44
46
  animation,
45
47
  scrollEnabled,
@@ -63,7 +65,7 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
63
65
  const isVertical = orientation === "vertical";
64
66
 
65
67
  const maxPage = dataLength;
66
- const isHorizontal = useDerivedValue(() => !isVertical, [isVertical]);
68
+ const isHorizontal = useDerivedValue(() => !isVertical, webOnlyDependencies([isVertical]));
67
69
  const max = useSharedValue(0);
68
70
  const panOffset = useSharedValue<number | undefined>(undefined); // set to undefined when not actively in a pan gesture
69
71
  const touching = useSharedValue(false);
@@ -71,10 +73,11 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
71
73
  const hasStartedMovement = useSharedValue(false);
72
74
  const scrollEndTranslation = useSharedValue(0);
73
75
  const scrollEndVelocity = useSharedValue(0);
76
+ const sizeReadyDependencies = webOnlyDependencies([resolvedSize, sizePhase]);
74
77
  const sizeReady = useDerivedValue(() => {
75
78
  const currentSize = resolvedSize.value ?? 0;
76
79
  return sizePhase.value === "ready" && currentSize > 0;
77
- }, [resolvedSize, sizePhase]);
80
+ }, sizeReadyDependencies);
78
81
 
79
82
  // Get the limit of the scroll.
80
83
  const getLimit = React.useCallback(() => {
@@ -169,7 +172,11 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
169
172
  }
170
173
 
171
174
  if (snapMode === "nearest") {
172
- finalTranslation = withSpring(withProcessTranslation(-nextPage * currentSize), onFinished);
175
+ const nearestPage = -Math.round((origin + velocity * snapMomentum) / currentSize);
176
+ finalTranslation = withSpring(
177
+ withProcessTranslation(-nearestPage * currentSize),
178
+ onFinished
179
+ );
173
180
  }
174
181
 
175
182
  translation.value = finalTranslation;
@@ -183,7 +190,7 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
183
190
  return translation;
184
191
  }
185
192
  },
186
- [withSpring, resolvedSize, maxPage, loop, snapMode, translation]
193
+ [withSpring, resolvedSize, maxPage, loop, snapMode, snapMomentum, translation]
187
194
  );
188
195
 
189
196
  const onFinish = React.useCallback(
@@ -238,7 +245,7 @@ const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
238
245
  () => {
239
246
  if (snapMode !== "page") resetBoundary();
240
247
  },
241
- [resetBoundary, snapMode]
248
+ webOnlyDependencies([resetBoundary, snapMode])
242
249
  );
243
250
 
244
251
  const onGestureStart = useCallback(
@@ -18,6 +18,7 @@ import {
18
18
  positiveModulo,
19
19
  } from "../utils/carousel-math";
20
20
  import { dealWithAnimation } from "../utils/deal-with-animation";
21
+ import { webOnlyDependencies } from "../utils/web-only-dependencies";
21
22
 
22
23
  interface CarouselControllerOptions {
23
24
  ref: React.ForwardedRef<CarouselRef>;
@@ -71,7 +72,7 @@ export function useCarouselController(options: CarouselControllerOptions): Carou
71
72
  const liveRawIndex = useRef(rawDataLength > 0 ? defaultIndex : 0);
72
73
  const settledIndex = useRef(rawDataLength > 0 ? defaultIndex : 0);
73
74
  const warnedCommands = useRef(new Set<string>());
74
- const canMove = rawDataLength > 1;
75
+ const canMove = rawDataLength > 1 || (loop && rawDataLength === 1 && dataLength > 1);
75
76
 
76
77
  const warnOnce = React.useCallback((key: string, message: string) => {
77
78
  if (!__DEV__ || warnedCommands.current.has(key)) return;
@@ -106,7 +107,7 @@ export function useCarouselController(options: CarouselControllerOptions): Carou
106
107
  index.value = physicalIndex;
107
108
  scheduleOnRN(setLiveRawIndex, rawIndex);
108
109
  },
109
- [dataLength, handlerOffset, index, rawDataLength, size]
110
+ webOnlyDependencies([dataLength, handlerOffset, index, rawDataLength, size])
110
111
  );
111
112
 
112
113
  const settle = React.useCallback(
@@ -12,6 +12,7 @@ import {
12
12
  reconcileOffsetAfterDataChange,
13
13
  } from "../utils/carousel-math";
14
14
  import { computeOffsetIfSizeChanged } from "../utils/compute-offset-if-size-changed";
15
+ import { webOnlyDependencies } from "../utils/web-only-dependencies";
15
16
 
16
17
  export type CarouselSizePhase = "pending" | "ready" | "updating";
17
18
 
@@ -97,7 +98,7 @@ export function useCommonVariables(
97
98
  scheduleOnRN(syncSize, current);
98
99
  }
99
100
  },
100
- [syncSize]
101
+ webOnlyDependencies([syncSize])
101
102
  );
102
103
 
103
104
  React.useEffect(() => {
@@ -242,7 +243,7 @@ export function useCommonVariables(
242
243
  prevSize.value = currentSize;
243
244
  sizePhase.value = "ready";
244
245
  },
245
- [dataLength, defaultIndex, hasInitializedOffset, resolvedSize, sizePhase]
246
+ webOnlyDependencies([dataLength, defaultIndex, hasInitializedOffset, resolvedSize, sizePhase])
246
247
  );
247
248
 
248
249
  return {
@@ -20,6 +20,7 @@ export type InitializedCarouselProps<Item> = Omit<
20
20
  | "overscrollEnabled"
21
21
  | "scrollEnabled"
22
22
  | "snapMode"
23
+ | "snapMomentum"
23
24
  | "style"
24
25
  > & {
25
26
  autoplay: boolean;
@@ -32,6 +33,7 @@ export type InitializedCarouselProps<Item> = Omit<
32
33
  overscrollEnabled: boolean;
33
34
  scrollEnabled: boolean;
34
35
  snapMode: CarouselSnapMode;
36
+ snapMomentum: number;
35
37
  style: NonNullable<CarouselProps<Item>["style"]>;
36
38
  animation: CarouselAnimation;
37
39
  rawData: Item[];
@@ -57,6 +59,7 @@ export function useInitProps<Item>(props: CarouselProps<Item>): InitializedCarou
57
59
  });
58
60
  const scrollEnabled = props.scrollEnabled ?? true;
59
61
  const snapMode = props.snapMode ?? "page";
62
+ const snapMomentum = props.snapMomentum ?? 2;
60
63
  const overscrollEnabled = props.overscrollEnabled ?? true;
61
64
  const style = props.style ?? {};
62
65
  const animation: CarouselAnimation = props.animation ?? {
@@ -123,6 +126,7 @@ export function useInitProps<Item>(props: CarouselProps<Item>): InitializedCarou
123
126
  overscrollEnabled,
124
127
  scrollEnabled,
125
128
  snapMode,
129
+ snapMomentum,
126
130
  style,
127
131
  animation,
128
132
  data,
@@ -154,6 +158,15 @@ function validateStaticProps<Item>(props: CarouselProps<Item>) {
154
158
  );
155
159
  }
156
160
 
161
+ if (
162
+ props.snapMomentum !== undefined &&
163
+ (!Number.isFinite(props.snapMomentum) || props.snapMomentum < 0)
164
+ ) {
165
+ throw new Error(
166
+ "[react-native-reanimated-carousel] snapMomentum must be a non-negative number."
167
+ );
168
+ }
169
+
157
170
  if (
158
171
  props.renderWindowSize !== undefined &&
159
172
  (!Number.isInteger(props.renderWindowSize) || props.renderWindowSize <= 0)
@@ -1,6 +1,7 @@
1
1
  import type { SharedValue } from "react-native-reanimated";
2
2
  import { Extrapolation, interpolate, useDerivedValue } from "react-native-reanimated";
3
3
 
4
+ import { webOnlyDependencies } from "../utils/web-only-dependencies";
4
5
  import type { VisibleRangesValue } from "./useVisibleRanges";
5
6
 
6
7
  export interface OffsetOptions {
@@ -38,6 +39,15 @@ export const useOffsetX = (opts: OffsetOptions, visibleRanges: VisibleRangesValu
38
39
  const MAX = positiveCount * size;
39
40
  const MIN = -((VALID_LENGTH - positiveCount) * size);
40
41
 
42
+ const xDependencies = webOnlyDependencies([
43
+ loop,
44
+ dataLength,
45
+ viewCount,
46
+ type,
47
+ size,
48
+ visibleRanges,
49
+ handlerOffset,
50
+ ]);
41
51
  const x = useDerivedValue(() => {
42
52
  if (!size) return 0;
43
53
 
@@ -75,7 +85,7 @@ export const useOffsetX = (opts: OffsetOptions, visibleRanges: VisibleRangesValu
75
85
  }
76
86
 
77
87
  return Number.MAX_SAFE_INTEGER;
78
- }, [loop, dataLength, viewCount, type, size, visibleRanges, handlerOffset]);
88
+ }, xDependencies);
79
89
 
80
90
  return x;
81
91
  };
@@ -4,6 +4,7 @@ import { scheduleOnRN } from "react-native-worklets";
4
4
 
5
5
  import type { CarouselProgressChangeHandler } from "../types";
6
6
  import { getLogicalProgress } from "../utils/carousel-math";
7
+ import { webOnlyDependencies } from "../utils/web-only-dependencies";
7
8
 
8
9
  export function useOnProgressChange(options: {
9
10
  loop: boolean;
@@ -28,6 +29,6 @@ export function useOnProgressChange(options: {
28
29
  if (progress) progress.value = nextProgress;
29
30
  if (onProgressChange) scheduleOnRN(onProgressChange, nextProgress);
30
31
  },
31
- [loop, offset, onProgressChange, progress, rawDataLength, size, sizeReady]
32
+ webOnlyDependencies([loop, offset, onProgressChange, progress, rawDataLength, size, sizeReady])
32
33
  );
33
34
  }
@@ -2,6 +2,8 @@ import { useRef } from "react";
2
2
  import type { SharedValue } from "react-native-reanimated";
3
3
  import { useDerivedValue } from "react-native-reanimated";
4
4
 
5
+ import { webOnlyDependencies } from "../utils/web-only-dependencies";
6
+
5
7
  type Range = [number, number];
6
8
 
7
9
  export interface VisibleRanges {
@@ -11,7 +13,7 @@ export interface VisibleRanges {
11
13
 
12
14
  export type VisibleRangesValue = SharedValue<VisibleRanges>;
13
15
 
14
- function normalizeWindowSize(total: number, windowSize?: number) {
16
+ export function normalizeWindowSize(total: number, windowSize?: number) {
15
17
  "worklet";
16
18
 
17
19
  return typeof windowSize === "number" && Number.isFinite(windowSize) && windowSize > 0
@@ -100,6 +102,7 @@ export function useVisibleRanges(options: {
100
102
  const windowSize = normalizeWindowSize(total, _windowSize);
101
103
  const cachedRanges = useRef<VisibleRanges | null>(null);
102
104
 
105
+ const rangesDependencies = webOnlyDependencies([loop, total, windowSize, translation, viewSize]);
103
106
  const ranges = useDerivedValue(() => {
104
107
  if (total <= 0) {
105
108
  return computeVisibleRanges({ total, currentIndex: 0, windowSize, loop });
@@ -128,7 +131,7 @@ export function useVisibleRanges(options: {
128
131
 
129
132
  cachedRanges.current = newRanges;
130
133
  return newRanges;
131
- }, [loop, total, windowSize, translation, viewSize]);
134
+ }, rangesDependencies);
132
135
 
133
136
  return ranges;
134
137
  }
@@ -1,3 +1,4 @@
1
+ import type { ViewStyle } from "react-native";
1
2
  import { interpolate } from "react-native-reanimated";
2
3
 
3
4
  import type { CarouselDirectionSign } from "../utils/carousel-direction";
@@ -10,7 +11,7 @@ export function normalLayout(opts: {
10
11
  }) {
11
12
  const { size, isVertical, directionSign } = opts;
12
13
 
13
- return (value: number) => {
14
+ return (value: number): ViewStyle => {
14
15
  "worklet";
15
16
  const logicalTranslate = interpolate(value, [-1, 0, 1], [-size, 0, size]);
16
17
  const translate = isVertical
@@ -1,3 +1,4 @@
1
+ import type { ViewStyle } from "react-native";
1
2
  import { Extrapolation, interpolate } from "react-native-reanimated";
2
3
 
3
4
  import type { CarouselLayout } from "../types";
@@ -17,7 +18,7 @@ export function parallaxLayout(
17
18
  const { size, isVertical, directionSign } = baseConfig;
18
19
  const { offset = 100, scale = 0.8, adjacentScale = scale ** 2 } = config;
19
20
 
20
- return (relativeProgress: number) => {
21
+ return (relativeProgress: number): ViewStyle => {
21
22
  "worklet";
22
23
  const logicalTranslate = interpolate(
23
24
  relativeProgress,
@@ -76,6 +76,7 @@ export type CarouselProps<Item> = {
76
76
  itemSize?: number;
77
77
  scrollEnabled?: boolean;
78
78
  snapMode?: CarouselSnapMode;
79
+ snapMomentum?: number;
79
80
  overscrollEnabled?: boolean;
80
81
  renderWindowSize?: number;
81
82
  animation?: CarouselAnimation;
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
 
3
- import { SharedValue, useSharedValue } from "react-native-reanimated";
3
+ import { type SharedValue, useSharedValue } from "react-native-reanimated";
4
4
  import type { CarouselCommonVariables } from "../hooks/useCommonVariables";
5
5
  import type { InitializedCarouselProps } from "../hooks/useInitProps";
6
6
 
@@ -0,0 +1,10 @@
1
+ import { Platform } from "react-native";
2
+
3
+ /**
4
+ * Reanimated only reads hook dependency arrays in its Web implementation when the
5
+ * worklets Babel plugin is not used. Native derives them from the worklet closure
6
+ * and, since Reanimated 4.6, warns on every render when an array is passed.
7
+ */
8
+ export function webOnlyDependencies<T extends unknown[]>(dependencies: T): T | undefined {
9
+ return Platform.OS === "web" ? dependencies : undefined;
10
+ }