Skip to content

BottomSheet mounts invisible on Android: containerHeight stuck at sentinel (Reanimated 4 reaction misses raw->container sync) #2688

Description

@christian-apollo

Bug

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 dashboard/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.

Captured evidence

Internal logging on a failing Android cold start (custom print added to isLayoutCalculated):

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

rawContainerHeight = 832, but containerHeight is still -999, detents never normalize, and animateToPosition is never called.

Environment

  • @gorhom/bottom-sheet 5.2.14 (current latest)
  • react-native-reanimated 4.4.0
  • react-native-gesture-handler 2.31.1
  • React Native 0.81, Android (Samsung A54, Android 14)
  • Non-modal BottomSheet, enableDynamicSizing={false}, snapPoints={[<px>, '100%']}, initial index={0}

Reproduction

Mount a non-modal BottomSheet with index={0} as part of a screen that also kicks off heavy synchronous JS work at mount (so the container onLayout is delayed). On Android the sheet is frequently invisible until something forces a re-layout.

Note: animateOnMount={false} is not a workaround — it makes it worse: it initializes didAnimateOnMount = true, and the layout-ready branch that calls setToPosition is gated on !didAnimateOnMount, so the sheet is never positioned at all (and snapToIndex(0) early-exits because index === nextIndex).

Proposed fix

Seed a non-sentinel containerHeight in the hosting container's layout handler, in the same modify that writes rawContainerHeight, only while it is still the sentinel. The existing reaction keeps refining it (e.g. modal vertical inset) whenever it does run, so normal behavior is unchanged. PR attached.

Metadata

Metadata

Assignees

No one assigned

    Labels

    invalidThis doesn't seem right

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions