Summary
On Android (New Architecture), rapidly remounting a ModalBottomSheet (reopening a sheet while the previous instance is still tearing down) can kill the whole React surface with:
com.facebook.react.bridge.RetryableMountingLayerException: Unable to find viewState for tag <N>. Surface stopped: false
at com.facebook.react.fabric.mounting.SurfaceMountingManager.getViewState(SurfaceMountingManager.kt:1057)
at com.facebook.react.fabric.mounting.SurfaceMountingManager.addViewAt(SurfaceMountingManager.kt:292)
at com.facebook.react.fabric.mounting.mountitems.IntBufferBatchMountItem.execute(IntBufferBatchMountItem.kt:122)
Because the failure is in the batch-mount path (only view commands are retried), React Native rethrows and destroys the ReactHost: the app is left on a dead white screen (process alive, touches dead) or hard-crashes if it happens inside expo-updates' error-recovery window. In our production app this reached ~2% of Android sessions once a high-frequency action sheet moved onto the portal path.
Root cause
Three ancestor wrappers of the native BottomSheetView are layout-only views that Fabric flattens:
BottomSheetProvider's PortalHost entry wrapper: <View style={StyleSheet.absoluteFill} pointerEvents="box-none">
BottomSheet's outer wrapper: <View style={absoluteFill} pointerEvents={modal ? (isCollapsed ? 'none' : 'auto') : 'box-none'}>
BottomSheet's inner wrapper: <View pointerEvents="box-none" style={absoluteFill}>
Wrapper 2's pointerEvents flips 'none' ↔ 'auto' on every open/collapse, which forces Fabric to unflatten it mid-churn. Under rapid portal entry remount, the resulting reparenting batch can arrive without the wrapper's Create mutation (an RN Fabric differ bug this shape exposes — being reported to facebook/react-native separately). Captured mount batch at crash time, with the surface registry identifying every tag:
REMOVE [2180]->[14] @3 // 2180 = BottomSheetView, 14 = provider-level parent
INSERT [2184]->[14] @3 // 2184 = the entry wrapper — has NO viewState (no Create!)
INSERT [2180]->[2184] @0 // existing sheet reparented into the wrapper
UPDATE LAYOUT [2184]->[14]: x:0 y:0 w:1080 h:2316
Causal confirmation: after pinning wrapper 1 with collapsable={false}, the identical crash moved exactly one level down to wrapper 2 (REMOVE [3010]->[3016], INSERT [3014]->[3016], INSERT [3010]->[3014] — 3016 now a real view, 3014 the outer sheet wrapper). Pinning all three eliminated it: 40 rapid-fire reproductions before the fix reproduced within ~3 attempts, zero after.
Notably the content views below the native view already set collapsable={false} — only the ancestors were left flattenable.
Repro sketch
- Android, New Architecture, RN 0.85.x, library 0.15.3 (the wrappers are unchanged on current
main).
- A
ModalBottomSheet opened from a list row's overflow menu (our case: an action sheet).
- Open the sheet, trigger an action that closes it and re-renders the host list, and reopen the sheet before the previous instance finishes tearing down. Fast repeated open/close of the same sheet component is the reliable trigger; on a Galaxy S23 it reproduced within ~3 attempts.
Fix
collapsable={false} on the three wrappers (Android-only semantics, no-op on iOS): never flattened means never unflattened, so the buggy mutation sequence cannot be emitted. PR attached.
Environment
- @swmansion/react-native-bottom-sheet 0.15.3 (wrapper shape identical on main / 0.16.x)
- react-native 0.85.3, New Architecture (bridgeless), Expo SDK 56
- Android 13–17, reproduced on Galaxy S23 Ultra; production telemetry across Samsung/Pixel/Motorola/OPPO
Summary
On Android (New Architecture), rapidly remounting a
ModalBottomSheet(reopening a sheet while the previous instance is still tearing down) can kill the whole React surface with:Because the failure is in the batch-mount path (only view commands are retried), React Native rethrows and destroys the ReactHost: the app is left on a dead white screen (process alive, touches dead) or hard-crashes if it happens inside expo-updates' error-recovery window. In our production app this reached ~2% of Android sessions once a high-frequency action sheet moved onto the portal path.
Root cause
Three ancestor wrappers of the native
BottomSheetVieware layout-only views that Fabric flattens:BottomSheetProvider's PortalHost entry wrapper:<View style={StyleSheet.absoluteFill} pointerEvents="box-none">BottomSheet's outer wrapper:<View style={absoluteFill} pointerEvents={modal ? (isCollapsed ? 'none' : 'auto') : 'box-none'}>BottomSheet's inner wrapper:<View pointerEvents="box-none" style={absoluteFill}>Wrapper 2's
pointerEventsflips'none' ↔ 'auto'on every open/collapse, which forces Fabric to unflatten it mid-churn. Under rapid portal entry remount, the resulting reparenting batch can arrive without the wrapper's Create mutation (an RN Fabric differ bug this shape exposes — being reported to facebook/react-native separately). Captured mount batch at crash time, with the surface registry identifying every tag:Causal confirmation: after pinning wrapper 1 with
collapsable={false}, the identical crash moved exactly one level down to wrapper 2 (REMOVE [3010]->[3016],INSERT [3014]->[3016],INSERT [3010]->[3014]— 3016 now a real view, 3014 the outer sheet wrapper). Pinning all three eliminated it: 40 rapid-fire reproductions before the fix reproduced within ~3 attempts, zero after.Notably the content views below the native view already set
collapsable={false}— only the ancestors were left flattenable.Repro sketch
main).ModalBottomSheetopened from a list row's overflow menu (our case: an action sheet).Fix
collapsable={false}on the three wrappers (Android-only semantics, no-op on iOS): never flattened means never unflattened, so the buggy mutation sequence cannot be emitted. PR attached.Environment