Report the window safe area insets through Dimensions - #58110
Draft
janicduplessis wants to merge 3 commits into
Draft
janicduplessis wants to merge 3 commits into
janicduplessis wants to merge 3 commits into
Conversation
This was referenced Aug 24, 2026
EventEmitter::experimental_flushSync only requests a beat, processed at the next EventBeat::induce. On iOS the run loop observer that induces the beat runs before Core Animation's commit observer, so a request made from layoutSubviews — inside CA's commit cycle — is only processed one frame later. AppleEventBeat now additionally schedules an induce in the display phase of the current commit cycle. Core Animation runs a commit as layout → display → commit, so a zero-sized layer marked as needing display during layout has its display called after the whole layout pass and before the transaction is committed. The layer is attached to the window of the requesting view: experimental_flushSync carries the tag of the emitting view — cached from its ShadowNodeFamily when the family is attached at creation, before the emitter is published, so reading it takes no lock — through EventDispatcher and EventQueue to EventBeat::requestSynchronous, with kNoTag meaning no view attribution; a no-argument overload keeps unattributed requesters unchanged. AppleEventBeat resolves the tag to the view's window layer through a resolver injected by RCTSurfacePresenter (findComponentViewWithTag: on the mounting registry, a nullable, non-creating, main-thread lookup). The requesting view's window is by definition the root of the layer tree whose layout emitted the request, so the flusher is guaranteed a display phase in the current commit cycle, including for content UIKit mounts in a window of its own, like a full screen modal or LogBox. Requests from several windows in one cycle each dirty their own layer; the first display to fire drains the queue and the rest no-op on the request flag. VirtualView's synchronous flushes get the same targeting through their own emitter. A related fix in EventBeat itself: a synchronous request is no longer stranded behind an already-scheduled asynchronous beat (it would silently lose its this-frame guarantee, and the leftover flag would make an unrelated later beat blocking). AppleEventBeat.cpp becomes .mm for the Objective-C. Covered by new unit tests in EventBeatTest.cpp, which drive the protected induce through a subclass standing in for the platform. The C++ API snapshots are regenerated; the deltas are the requestSynchronous overload pair, EventEmitter::getTag, the resolver type, and the AppleEventBeat constructor and destructor.
Reports the part of a view that is covered by the system UI, as a view prop:
```jsx
<View
experimental_onSafeAreaInsetsChange={({nativeEvent: {insets, frame}}) => {
// insets: {top, right, bottom, left}, frame: {x, y, width, height}
}}
/>
```
`SafeAreaView` is deprecated in favour of `react-native-safe-area-context`,
but core surfaces like LogBox and the element inspector cannot depend on the
library, so core keeps a private copy of the deprecated component alive. The
smallest primitive that lets both sides go away is native code reporting
inset values to JavaScript — today the library's own `RNCSafeAreaProvider`
component. This adds that primitive, with the payload the library already
uses, so `SafeAreaProvider` can swap its native component for a plain `View`.
Insets are relative to the view: one laid out inside the safe area reports
zeros. That is what makes the prop composable and stops nested providers
from double-padding.
**Cost when unused.** The prop is a `bool` in `BaseViewProps`, like
`onLayout`; native only observes the safe area when it is set. On iOS the
flag is read from the props the view already holds and the last-sent insets
live behind a single pointer ivar that stays nil unless the view observes;
the only unconditional cost is a branch in `layoutSubviews`,
`didMoveToWindow` and `safeAreaInsetsDidChange`.
**Cost when used.** Events fire only when the *insets* change — the frame is
in the payload but not in the trigger — so a view moving inside a scroll
view emits nothing, and 50 observing rows scroll at the same frame times as
zero. An observing view allocates nothing per frame on Android in the steady
state. Benchmarked with the "Scroll benchmark" section of the new RNTester
example.
**Synchronous dispatch.** The event goes out through
`EventEmitter::experimental_flushSync` as a `Discrete` event, so inset-driven
layout is mounted in the frame the insets changed in — first mount included,
and on rotation the padding animates with the transition instead of jumping
after it.
Edge cases covered: view flattening (the prop forms a stacking context so
the host view cannot be optimized away), view recycling on both platforms,
Android views fully clipped by an ancestor, and multi-window iPad.
Folded in from review: the prop is forwarded through BaseViewManagerDelegate
for components with generated delegates, and the event is exported from the
native view config so it maps to the handler when native view configs are
in use.
`Dimensions.get('window').experimental_safeAreaInsets` (and
`useWindowDimensions`) reports the safe area insets of the window, using the
same native computation as the view prop applied to the window itself.
Unlike the prop, this is available synchronously at startup — no event has to
arrive first — and it updates through the existing `change` event. That is
what the safe area context library needs `initialWindowMetrics` for, its last
remaining native module, and it is what lets a component render padded on its
very first frame instead of correcting itself once the first inset event
lands.
The field is absent on platforms and versions that cannot report it, rather
than reported as zeros, so a consumer can tell "no insets" from "unknown".
janicduplessis
force-pushed
the
safe-area/3-window-insets-in-dimensions
branch
from
September 16, 2026 13:50
f6e4a96 to
f8b6613
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
Dimensions.get('window').experimental_safeAreaInsets(anduseWindowDimensions) reports the safe area insets of the window, using the same native computation as the view prop applied to the window itself.Unlike the prop, this is available synchronously at startup — no event has to arrive first — and it updates through the existing
changeevent. That is whatreact-native-safe-area-contextneedsinitialWindowMetricsfor, its last remaining native module, and it is what lets a component render padded on its first frame instead of correcting itself once the first inset event lands.The field is absent, rather than zeroed, on platforms and versions that cannot report it, so a consumer can tell "no insets" from "unknown".
Prefixed members are stripped from
ReactNativeApi.d.ts, so the public API snapshot is unchanged; the generated TypeScript types carry the field.Stacked on #58109 — Android reads the insets through the observer that PR adds. This PR's diff includes the ones below it until they merge; review the top commit.
Changelog:
[GENERAL] [ADDED] - Report the window safe area insets as
Dimensions.get('window').experimental_safeAreaInsetsTest Plan:
covering the physical-pixels-to-dp scaling and that the field is absent when native does not report it.
In RNTester, the new "Window safe area insets from Dimensions" section reports the same values as the view prop does for a full-screen view, on an iPhone 17 Pro simulator and an Android 16 emulator — screenshots in #57967.
Stack — split out of #57967, which stays open as the prototype and design discussion. GitHub will not take a fork branch as a pull request base, so each of these targets
mainand its diff contains the ones below it until they merge. Each PR is one commit on top of the previous one.This PR's own change, without the ones below it: 14 files.
👉 3. #58110 — Report the window safe area insets through Dimensions
4. #58111 — Warn in development when a view reports its safe area insets in a loop
5. #58112 — Render the internal SafeAreaView from the safe area insets prop
6. #58113 — Remove the native SafeAreaView and the deprecated public export