Summary
On iOS with the new architecture, when the OS launches the app in the background (CoreBluetooth state restoration, a CoreLocation region entry), initialWindowMetrics is null and SafeAreaProvider then depends entirely on one onInsetsChange event from RNCSafeAreaProviderComponentView. In that launch path the event is mostly lost, so SafeAreaProvider keeps insets === null and renders no children. When the user later opens the app they get a bare window (black in a dark theme, white in a light one) until they force quit. We had riders of a production app reporting exactly this as a "black screen when reopening the app".
Why initialWindowMetrics is null
RNCSafeAreaContext.getConstants reads RCTKeyWindow(). React Native's RCTKeyWindow only returns a window from a scene in foregroundActive or foregroundInactive state. A background launch has neither, so the constants carry initialWindowMetrics: null. That part is expected; the problem is what happens next.
Why the event is lost
RNCSafeAreaProviderComponentView.invalidateSafeAreaInsets sends once per view (_initialInsetsSent) and afterwards only when the insets or frame change. In a background launch the first layout passes happen while the app is not on screen, and by the time the app comes to the foreground the view's insets and frame no longer change, so no further event is sent. A recycled view whose frame does not change on re-use has the same problem for a remounted JS SafeAreaProvider, which starts from insets === null again.
Reproduction (iOS 26.4 simulator, deterministic enough)
- App with
SafeAreaProvider initialMetrics={initialWindowMetrics} at the root and the location background mode, monitoring a CLCircularRegion.
xcrun simctl location <sim> set a point 15 km outside the region, xcrun simctl terminate the app, then set the location inside the region. iOS relaunches the app in the background within about 20 s.
xcrun simctl launch the app to bring it to the foreground.
Result on our app: blank window in 4 of 5 cycles. Inspecting the React tree over the Hermes inspector shows SafeAreaProvider with insets === null and no child fibers. Rotating the simulator forces a layout pass and the UI appears immediately, which confirms the provider was only waiting for the event.
Environment
- react-native-safe-area-context 5.9.1
- react-native 0.86.3, new architecture (Fabric), Hermes
- iOS 26.4 simulator and iOS 26.6.1 devices
Proposed fix (what we ship as a patch-package)
Make sure every JS instance hears from the native view at least once, regardless of whether a layout pass would otherwise happen:
// RNCSafeAreaProviderComponentView.mm
- (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter
{
[super updateEventEmitter:eventEmitter];
// A new JS instance (remount, or a recycled native view) must get its own send.
_initialInsetsSent = NO;
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
// Ask for a layout pass rather than sending here: safeAreaInsets are only
// valid once the view has been laid out in the window, and a frame that did
// not change would otherwise get no layout pass at all.
if (self.window != nil) {
[self setNeedsLayout];
}
}
With this patch the same 5 relaunch cycles rendered 5 of 5, and a normal launch still produces the same small number of onInsetsChange sends (no extra traffic on background and foreground transitions).
On the JS side we also stopped passing null as initialMetrics: we persist the last insets the provider settled on and pass those (on the window frame from Dimensions) when initialWindowMetrics is null, so the tree renders immediately and the native report corrects it. It may be worth documenting that initialWindowMetrics is null on background launches, since the README presents it as a way to render on the first frame.
Happy to open a PR with the native change if you would take it.
Summary
On iOS with the new architecture, when the OS launches the app in the background (CoreBluetooth state restoration, a CoreLocation region entry),
initialWindowMetricsisnullandSafeAreaProviderthen depends entirely on oneonInsetsChangeevent fromRNCSafeAreaProviderComponentView. In that launch path the event is mostly lost, soSafeAreaProviderkeepsinsets === nulland renders no children. When the user later opens the app they get a bare window (black in a dark theme, white in a light one) until they force quit. We had riders of a production app reporting exactly this as a "black screen when reopening the app".Why
initialWindowMetricsis nullRNCSafeAreaContext.getConstantsreadsRCTKeyWindow(). React Native'sRCTKeyWindowonly returns a window from a scene inforegroundActiveorforegroundInactivestate. A background launch has neither, so the constants carryinitialWindowMetrics: null. That part is expected; the problem is what happens next.Why the event is lost
RNCSafeAreaProviderComponentView.invalidateSafeAreaInsetssends once per view (_initialInsetsSent) and afterwards only when the insets or frame change. In a background launch the first layout passes happen while the app is not on screen, and by the time the app comes to the foreground the view's insets and frame no longer change, so no further event is sent. A recycled view whose frame does not change on re-use has the same problem for a remounted JSSafeAreaProvider, which starts frominsets === nullagain.Reproduction (iOS 26.4 simulator, deterministic enough)
SafeAreaProvider initialMetrics={initialWindowMetrics}at the root and thelocationbackground mode, monitoring aCLCircularRegion.xcrun simctl location <sim> seta point 15 km outside the region,xcrun simctl terminatethe app, then set the location inside the region. iOS relaunches the app in the background within about 20 s.xcrun simctl launchthe app to bring it to the foreground.Result on our app: blank window in 4 of 5 cycles. Inspecting the React tree over the Hermes inspector shows
SafeAreaProviderwithinsets === nulland no child fibers. Rotating the simulator forces a layout pass and the UI appears immediately, which confirms the provider was only waiting for the event.Environment
Proposed fix (what we ship as a patch-package)
Make sure every JS instance hears from the native view at least once, regardless of whether a layout pass would otherwise happen:
With this patch the same 5 relaunch cycles rendered 5 of 5, and a normal launch still produces the same small number of
onInsetsChangesends (no extra traffic on background and foreground transitions).On the JS side we also stopped passing
nullasinitialMetrics: we persist the last insets the provider settled on and pass those (on the window frame fromDimensions) wheninitialWindowMetricsis null, so the tree renders immediately and the native report corrects it. It may be worth documenting thatinitialWindowMetricsis null on background launches, since the README presents it as a way to render on the first frame.Happy to open a PR with the native change if you would take it.