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
Version
v5
Reanimated Version
v3
Gesture Handler Version
v2
Platforms
Android
What happened?
On Android, a non-modal
BottomSheetwith an initialindex >= 0intermittently 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 containeronLayoutlands a second or two after mount. Under that contention it reproduces ~every time; in lighter conditions it's intermittent.Root cause
useAnimatedLayoutkeeps the measured container height in two fields of the layout state:rawContainerHeight(written directly byBottomSheetHostingContainer'sonLayout) andcontainerHeight(the effective value everything else reads).containerHeightis derived fromrawContainerHeightby auseAnimatedReaction:On Reanimated 4 this reaction's handler can miss the initial
INITIAL_LAYOUT_VALUE (-999)→ measured-height transition (it does not fire for the firstlayoutState.modify). When that happens,containerHeightstays at the sentinel even thoughrawContainerHeightis correct.Consequences cascade from there:
useAnimatedDetentsearly-exits (if (containerHeight === INITIAL_LAYOUT_VALUE) return {}), sodetentsisundefined.isLayoutCalculatednever becomestrue(it requires normalized detents).animateToPosition/setToPosition, soanimatedPositionstays at its initialDimensions.get('window').height— i.e. fully off screen.Notably,
useDerivedValueconsumers of the same layout state do recompute on themodify; only theuseAnimatedReactionhandler is skipped. That points at a Reanimated-4 reaction-vs-modifytiming issue rather than app code.Reproduction steps
<BottomSheet index={0}>(with a couple ofsnapPoints) on a screen that mounts during a cold app start while the JS thread is busy.isLayoutCalculatedand confirmcontainerHeight === -999whilerawContainerHeightalready holds the measured height.Reproduction sample
https://snack.expo.dev/@gorhom/bottom-sheet---issue-reproduction-template
Relevant log output