Skip to content

[Bug]: BottomSheet mounts invisible on Android — containerHeight stuck at sentinel (Reanimated 4 reaction misses raw->container sync) #2690

Description

@christian-apollo

Version

v5

Reanimated Version

v3

Gesture Handler Version

v2

Platforms

Android

What happened?

On Android, a non-modal BottomSheet with an initial index >= 0 intermittently mounts invisible — the component and its children are in the tree, but the sheet is positioned entirely off screen (at window height / closed) and never animates in. It happens most often on a cold app start when the screen mounts while the JS thread is busy, so the container onLayout lands a second or two after mount. Under that contention it reproduces ~every time; in lighter conditions it's intermittent.

Root cause

useAnimatedLayout keeps the measured container height in two fields of the layout state: rawContainerHeight (written directly by BottomSheetHostingContainer's onLayout) and containerHeight (the effective value everything else reads). containerHeight is derived from rawContainerHeight by a useAnimatedReaction:

useAnimatedReaction(
  () => state.value.rawContainerHeight,
  (result, previous) => {
    if (result === previous) return;
    if (result === INITIAL_LAYOUT_VALUE) return;
    state.modify(_state => {
      'worklet';
      _state.containerHeight = modal ? result - verticalInset : result;
      return _state;
    });
  },
  [state, verticalInset, modal]
);

On Reanimated 4 this reaction's handler can miss the initial INITIAL_LAYOUT_VALUE (-999) → measured-height transition (it does not fire for the first layoutState.modify). When that happens, containerHeight stays at the sentinel even though rawContainerHeight is correct.

Consequences cascade from there:

  • useAnimatedDetents early-exits (if (containerHeight === INITIAL_LAYOUT_VALUE) return {}), so detents is undefined.
  • isLayoutCalculated never becomes true (it requires normalized detents).
  • The mount positioning reaction never runs animateToPosition / setToPosition, so animatedPosition stays at its initial Dimensions.get('window').height — i.e. fully off screen.

Notably, useDerivedValue consumers of the same layout state do recompute on the modify; only the useAnimatedReaction handler is skipped. That points at a Reanimated-4 reaction-vs-modify timing issue rather than app code.

Note: I'm actually on react-native-reanimated 4.4.0; the version dropdown above tops out at v3, so I selected the closest non-deprecated option.

Reproduction steps

  • Render a non-modal <BottomSheet index={0}> (with a couple of snapPoints) on a screen that mounts during a cold app start while the JS thread is busy.
  • Observe that the sheet mounts off screen and never animates in.
  • Add logging to isLayoutCalculated and confirm containerHeight === -999 while rawContainerHeight already holds the measured height.

Reproduction sample

https://snack.expo.dev/@gorhom/bottom-sheet---issue-reproduction-template

Relevant log output

[BottomSheetHostingContainer::handleLayoutEvent] height:832
[isLayoutCalc] containerH=-999 rawContainerH=832 handleH=0 detents=none -> false

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions