Repository navigation
Description
Version
v5
Reanimated Version
v3
Gesture Handler Version
v2
Platforms
iOS
What happened?
The template's dropdowns don't list Reanimated 4 or Gesture Handler 3 yet. Actual versions: @gorhom/bottom-sheet 5.2.14, Reanimated 4.7.1, react-native-worklets 0.13.0, Gesture Handler 3.3.0, Expo SDK 58, React Native 0.88.1 (New Architecture).
When the device rotates while a BottomSheetModal with topInset is open, animatedLayoutState.containerHeight keeps the inset from the previous orientation:
Rotation (iPhone 17 Pro, topInset={insets.top}) |
Expected containerHeight |
Actual | Effect |
|---|---|---|---|
| landscape → portrait (inset 0 → 62) | 812 (874 − 62) | 874 | footer pushed 62pt below the screen edge |
| portrait → landscape (inset 62 → 0) | 402 | 340 | footer floats 62pt above the bottom |
Closing and reopening the sheet in the new orientation always gives the right height. Any modal sheet whose topInset comes from the safe area is affected. It is timing dependent: now and then the first rotation after launch comes out right, and repeating the steps reproduces it.
Video (iPhone 17 Pro simulator, stock 5.2.14, both directions): https://github.com/abisalde/gorhom-sheet-rotation-repro/blob/main/media/rotation-bug.mp4
Cause. useAnimatedLayout subtracts topInset + bottomInset in the react callback of its two useAnimatedReactions, but their prepare callbacks return only the raw container height. On rotation the window height and the safe-area inset arrive in separate updates. When the inset changes, the reaction restarts with the new closure. The height it returns is equal to previous, because Reanimated keeps previous across restarts, so it returns early and the new inset is never applied. The [state, verticalInset, modal] dependency array doesn't help, because Reanimated ignores it on native (see #2785).
Suggested fix. Return the inset-adjusted height from prepare, so an inset change on its own produces a new value. Applied to both reactions:
useAnimatedReaction(
- () => containerLayoutState?.get().height,
- (result, previous) => {
- if (!result || result === previous) {
+ () => {
+ const height = containerLayoutState?.get().height;
+ if (!height || height === INITIAL_LAYOUT_VALUE) {
+ return INITIAL_LAYOUT_VALUE;
+ }
+ return modal ? height - verticalInset : height;
+ },
+ (result, previous) => {
+ if (result === previous) {
return;
}
if (result === INITIAL_LAYOUT_VALUE) {
return;
}
state.modify(_state => {
'worklet';
- _state.containerHeight = modal ? result - verticalInset : result;
+ _state.containerHeight = result;
return _state;
});
},
[state, verticalInset, modal]
);The rawContainerHeight reaction above it gets the same change. With this patch, both rotations in the repro give the expected heights. We run it as a pnpm patch in our app and checked it on iOS and Android, including foldables. It doesn't overlap with #2786, which only changes the initial state. Fix: #2794.
Reproduction steps
- Run the repro on an iPhone with a notch or Dynamic Island, with rotation lock off. The sheet prints
useBottomSheetInternal()values. - Rotate to landscape.
- Tap Open sheet.
- Rotate to portrait.
- Expected:
containerHeight 812, red footer fully visible. Actual:containerHeight 874, footer cut off at the bottom edge. - Reverse: close, open in portrait, rotate to landscape. Expected
402, actual340.
Reproduction sample
https://snack.expo.dev/o9tUM4R_D2_6QhVFzqEzR (SDK 55)
Snack cannot install @gorhom/bottom-sheet >= 5.1.8 from npm: its packager rejects the react-native-reanimated peer range >=3.16.0 || >=4.0.0- as non-semver (HTTP 422). So the Snack includes the published 5.2.14 src folder unchanged as local files, and App.tsx imports from it. Snack's SDK 56 runtime also fails to start in Expo Go 56.0.4 ("Tried to register two views with the same name RNSScreen"), hence SDK 55.
Same code as a standalone Expo SDK 58 / Reanimated 4.7.1 app, with the video: https://github.com/abisalde/gorhom-sheet-rotation-repro
This replaces #2792, which the template check closed because it linked only the repo.
Relevant log output
N/A: no errors; the wrong value is visible in the on-screen readout.
Activity