Skip to content

[Bug] Carousel gestures permanently stop working after parent view toggles display: 'none' #885

@pepperboy-proground

Description

@pepperboy-proground

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

  1. Place a <Carousel> inside a view that toggles visibility via display: 'none' / display: 'flex'
  2. Swipe the carousel — it works normally
  3. Toggle the parent view to display: 'none' (e.g., switch to another tab)
  4. Toggle it back to display: 'flex'
  5. 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:

  1. The onLayout callback (line 495-518) fires with zero dimensions
  2. However, when sizeExplicit is true (i.e., width prop is set), onLayout does NOT update resolvedSize (guarded at line 503)
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions