Describe the bug
When a Carousel is mounted inside a parent view that toggles between display: 'none' and display: 'flex' (e.g., tab-based navigation), the carousel's pan gesture becomes permanently unresponsive after the parent is hidden and shown again.
This is a regression introduced in 5.0.0-beta.3. The same setup works correctly in 5.0.0-beta.0.
Environment
react-native-reanimated-carousel: 5.0.0-beta.3 (works on 5.0.0-beta.0)
react-native-reanimated: 4.2.1
react-native-gesture-handler: ^2.28.0
react-native: 0.78
- Platform: iOS & Android
To Reproduce
- Place a
<Carousel> inside a view that toggles visibility via display: 'none' / display: 'flex'
- Swipe the carousel — it works normally
- Toggle the parent view to
display: 'none' (e.g., switch to another tab)
- Toggle it back to
display: 'flex'
- Try to swipe the carousel — gestures are completely unresponsive
Minimal reproduction
function TabScreen() {
const [tab, setTab] = useState<'carousel' | 'other'>('carousel');
return (
<View>
<Button title="Switch Tab" onPress={() => setTab(t => t === 'carousel' ? 'other' : 'carousel')} />
{/* Carousel tab */}
<View style={{ display: tab === 'carousel' ? 'flex' : 'none' }}>
<Carousel
width={screenWidth}
height={200}
data={items}
renderItem={({ item }) => <Card item={item} />}
/>
</View>
{/* Other tab */}
<View style={{ display: tab === 'other' ? 'flex' : 'none' }}>
<Text>Other content</Text>
</View>
</View>
);
}
Root Cause Analysis
The issue is in ScrollViewGesture.tsx. The sizeReady derived value and resolvedSize shared value are used as guards in all gesture callbacks (onGestureStart, onGestureUpdate, onGestureEnd, onGestureFinalize):
// ScrollViewGesture.tsx - line 82-85
const sizeReady = useDerivedValue(() => {
const currentSize = resolvedSize.value ?? 0;
return sizePhase.value === "ready" && currentSize > 0;
}, [resolvedSize, sizePhase]);
// ScrollViewGesture.tsx - line 306-314
const onGestureStart = useCallback((_) => {
"worklet";
const currentSize = resolvedSize.value ?? 0;
if (!sizeReady.value || currentSize <= 0) {
return; // <-- Gesture silently rejected!
}
// ...
}, [...]);
When display: 'none' is set on the parent:
- The
onLayout callback (line 495-518) fires with zero dimensions
- However, when
sizeExplicit is true (i.e., width prop is set), onLayout does NOT update resolvedSize (guarded at line 503)
- Despite this, the underlying native
GestureDetector gets detached from the gesture system when the view hierarchy is hidden
When display is set back to 'flex':
4. The GestureDetector's native handler may not properly re-attach to the gesture system
5. The measure(containerRef) call in getLimit() (line 94) may return null or zero dimensions because the native view's layout measurement is stale
6. All gesture events are silently dropped by the sizeReady / currentSize <= 0 guards
This did not happen in 5.0.0-beta.0 because the sizeReady / resolvedSize guard mechanism was added (or significantly changed) in beta.3.
Expected behavior
The carousel should resume responding to swipe gestures after its parent view becomes visible again (display: 'flex').
Workaround
Switching from display: 'none' to conditional rendering ({isVisible && <Carousel ... />}) resolves the issue, but this forces the carousel to unmount/remount on every tab switch, losing scroll position and triggering re-renders.
Another workaround is downgrading to 5.0.0-beta.0.
Describe the bug
When a Carousel is mounted inside a parent view that toggles between
display: 'none'anddisplay: 'flex'(e.g., tab-based navigation), the carousel's pan gesture becomes permanently unresponsive after the parent is hidden and shown again.This is a regression introduced in
5.0.0-beta.3. The same setup works correctly in5.0.0-beta.0.Environment
react-native-reanimated-carousel: 5.0.0-beta.3 (works on 5.0.0-beta.0)react-native-reanimated: 4.2.1react-native-gesture-handler: ^2.28.0react-native: 0.78To Reproduce
<Carousel>inside a view that toggles visibility viadisplay: 'none'/display: 'flex'display: 'none'(e.g., switch to another tab)display: 'flex'Minimal reproduction
Root Cause Analysis
The issue is in
ScrollViewGesture.tsx. ThesizeReadyderived value andresolvedSizeshared value are used as guards in all gesture callbacks (onGestureStart,onGestureUpdate,onGestureEnd,onGestureFinalize):When
display: 'none'is set on the parent:onLayoutcallback (line 495-518) fires with zero dimensionssizeExplicitis true (i.e.,widthprop is set),onLayoutdoes NOT updateresolvedSize(guarded at line 503)GestureDetectorgets detached from the gesture system when the view hierarchy is hiddenWhen
displayis set back to'flex':4. The
GestureDetector's native handler may not properly re-attach to the gesture system5. The
measure(containerRef)call ingetLimit()(line 94) may returnnullor zero dimensions because the native view's layout measurement is stale6. All gesture events are silently dropped by the
sizeReady/currentSize <= 0guardsThis did not happen in
5.0.0-beta.0because thesizeReady/resolvedSizeguard mechanism was added (or significantly changed) inbeta.3.Expected behavior
The carousel should resume responding to swipe gestures after its parent view becomes visible again (
display: 'flex').Workaround
Switching from
display: 'none'to conditional rendering ({isVisible && <Carousel ... />}) resolves the issue, but this forces the carousel to unmount/remount on every tab switch, losing scroll position and triggering re-renders.Another workaround is downgrading to
5.0.0-beta.0.