Remove the native SafeAreaView and the deprecated public export - #58113
Draft
janicduplessis wants to merge 4 commits into
Draft
janicduplessis wants to merge 4 commits into
janicduplessis wants to merge 4 commits into
Conversation
|
Warning JavaScript API change detected This PR commits an update to
This change was flagged as: |
This was referenced Aug 24, 2026
janicduplessis
force-pushed
the
safe-area/6-remove-native-safe-area-view
branch
from
August 24, 2026 22:04
ade0197 to
1c9df57
Compare
This was referenced Sep 14, 2026
janicduplessis
force-pushed
the
safe-area/6-remove-native-safe-area-view
branch
4 times, most recently
from
September 17, 2026 14:44
cac450b to
1f94c1e
Compare
meta-codesync Bot
pushed a commit
that referenced
this pull request
Sep 21, 2026
) Summary: `EventEmitter::experimental_flushSync` only *requests* an event beat; the beat is 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. Anything that reports layout-driven state to JS synchronously (`VirtualView` mode changes, and safe area insets in the PRs that build on this) renders a frame late in exactly the cases that matter. `AppleEventBeat` now also 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 gets its `display` call after the whole layout pass and before the transaction is committed. That layer needs to live in the tree being committed, so the beat has to know which tree that is — and the emitter tells it: - `experimental_flushSync` carries the **tag of the emitting view** through `EventDispatcher` and `EventQueue` to `EventBeat::requestSynchronous(Tag)`, with `kNoTag` (#58531) meaning no view attribution; a no-argument overload keeps unattributed requesters and the existing tests unchanged. The emitter reads the tag from its `ShadowNodeFamily` at flush time; `kNoTag` if the family is already gone. - `AppleEventBeat` resolves the tag to the layer of the view's **window** through a resolver injected by `RCTSurfacePresenter` (`findComponentViewWithTag:` on the mounting registry — nullable, non-creating, main thread) and attaches its flusher layer there. 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 within one cycle coalesce into a single induce. One 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. **Risk:** this changes when queued events are flushed on iOS for every `experimental_flushSync` caller — today `VirtualView`, and safe area insets with the PRs on top. The worst case is a beat processed a frame *earlier* than before, inside a Core Animation commit; the run loop observer path is untouched and still catches anything the display phase misses (an emitter with no tag, an unmounted view, a request off the main thread). Android ignores the tag. Revert is self-contained. ## Design Q&A: **What happens when two views in different windows update at once?** Each requesting window gets its own dirty flusher layer (the map is keyed by host layer), and the first `display` to fire induces the beat, which drains the whole event queue — every window's updates mount before that commit presents. The remaining flushers hit the `isEventBeatRequested_` guard and no-op, so it is one beat total, not one per window. If windows ever commit in separate transactions, each request still resolves within its own window's cycle, since its layer sits in the tree that emitted it. Only requesting windows carry a dirty layer. **Can the tag point at the wrong view — after an unmount, or a recycled view?** No. The tag comes from the emitter's `ShadowNodeFamily`, and a family keeps one tag for its whole life, across clones and state updates; if the family is already gone the flush carries `kNoTag` and skips the resolver. What changes over a view's life — its window — is read live: the tag resolves to a view at flush time and `view.window.layer` is looked up then, so a view that moved between windows targets its current tree. A view mid-unmount or recycled resolves to nil (the registry erases the entry and recycled views get tag `0`) and degrades to run-loop-observer timing. The tag only ever influences *where the induce is scheduled*, never what is delivered or to whom, so the blast radius of any staleness is one frame of timing, not correctness. **Does `VirtualView` need changes to benefit?** No — its sync mode-change flush goes through its own emitter, so the tag attribution is automatic. The case this improves is a mode change emitted during Core Animation layout (a resize pulling a virtualized item into view): on `main` that renders one frame late; here the induce lands in the display phase of the VirtualView's own window, including inside a full screen modal. ## Changelog: [INTERNAL] - Process synchronous event beats in the frame that requested them on iOS, scheduling the induce on the requesting view's window Pull Request resolved: #58530 Test Plan: New unit tests in `EventBeatTest.cpp` cover the beat semantics: a synchronous request during an already-scheduled asynchronous beat, coalescing, and induce ordering. They drive the protected `induce` through a subclass standing in for the platform. On device, with the safe area insets prop from the PRs above merged on top: an RNTester example renders a loud marker (yellow background) while a view observes the safe area but has not received an inset event yet, so any presented marker frame means the dispatch was not synchronous. The full apply → landscape → portrait sequence **inside a full screen modal** on an iPhone 17 Pro simulator, decomposed with ffmpeg into 982 frames and every frame scanned for the marker color — **zero marker frames**, and mid-rotation frames already carry the incoming orientation's insets, so the padding animates with the rotation. Scoped honestly: the first inset event after setting the prop is processed at the call site, so the marker primarily proves no regression; the same-frame path for layout-driven changes rests on the by-construction argument above plus the rotation frames. https://github.com/user-attachments/assets/0f2db837-c9c0-4457-96c2-847b7aecf10e `yarn fantom .../ViewSafeAreaInsets-itest.js` passes 4/4 with the prop merged on top. C++ API snapshots regenerated (`scripts/cxx-api/parser`, Doxygen 1.16.1): the deltas are the `requestSynchronous` overload pair, `EventEmitter::getTag`, the resolver type, and the `AppleEventBeat` constructor and destructor. --- **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 `main` and its diff contains the ones below it until they merge. Each PR is one commit on top of the previous one. This is the bottom of the stack, so its diff is already just this change. 👉 1. #58530 — Process synchronous event beats in the frame that requested them 2. #58109 — Add an `experimental_onSafeAreaInsetsChange` view prop 3. #58110 — Report the window safe area insets through Dimensions 4. #58112 — Render the internal SafeAreaView from the safe area insets prop 5. #58113 — Remove the native SafeAreaView and the deprecated public export An earlier variant that targeted the surface's root view instead of the view's window was closed in #58528; its review thread carries the analysis behind the window-based resolution. #58108 was the per-window predecessor this supersedes. Reviewed By: javache Differential Revision: D120200496 Pulled By: Abbondanzo fbshipit-source-id: b06ecb30935837abd6561f54674afef0cdf215a8
janicduplessis
force-pushed
the
safe-area/6-remove-native-safe-area-view
branch
2 times, most recently
from
September 21, 2026 16:21
7b1ae04 to
3a4a9bb
Compare
Reports the part of a view that is covered by the system UI, as a view prop:
```jsx
<View
experimental_onSafeAreaInsetsChange={({nativeEvent: {insets}}) => {
// insets: {top, right, bottom, left}
}}
/>
```
`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
are a plain `UIEdgeInsets` ivar (a negative sentinel marks "none sent yet",
since insets are never negative); the only unconditional cost is a branch in
`layoutSubviews`,
`didMoveToWindow` and `safeAreaInsetsDidChange`.
**Cost when used.** Events fire only when the *insets* change, 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.
Development warning for a view that reports its insets in a loop:
The system UI does not move many times a second, so a sustained stream of
inset events means the layout is feeding the insets back into the position of
the observed view: it is offset by the insets it reports, which moves it out
from under the system UI, which changes its insets. Every one of those events
renders synchronously, so the loop is paid for in frames.
`View` wraps the handler in development builds and warns once per view above
ten events in a second. The check lives in the handler `View` passes down
rather than in either platform's observer, so it covers iOS and Android with
one implementation and surfaces in LogBox with a JavaScript stack.
The production branch is the identity function, so the module stays out of
the bundle, and the native prop is unaffected either way — function props are
normalized to `true` before props are diffed, so wrapping does not produce an
update. Counts are kept per view in a `WeakMap` keyed by the event target, so
views that do not loop are never charged for it.
RNTester grows the mistake it warns about, and a Fantom test with a mocked
clock covers the rate, the once-per-view behaviour, per-view counting, and
that the handler still receives its event.
The payload is the insets alone. A frame is deliberately not included: it
would only be current as of the last inset change, since the trigger is
inset-only, and reporting it needs a coordinate space that differs between
platforms (the enclosing view controller on iOS, the window on Android). A
view that needs its own frame has `onLayout` and `measureInWindow`, which
stay current; the window's frame is in `Dimensions`.
`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".
The core surfaces that cannot depend on `react-native-safe-area-context` — LogBox, the element inspector, `InputAccessoryView` — get their safe area padding from a private component that until now wrapped the native `RCTSafeAreaView`. It applies the prop instead, in JavaScript. The initial insets are seeded from `Dimensions`, so the first frame is already padded, and the synchronous inset event then keeps them correct relative to the view. The seed is only exact for views aligned with the window edges, which these surfaces are. Two consequences, both visible in the updated LogBox snapshots: these surfaces now apply safe area padding on Android too, where they previously fell back to a plain `View`, and they re-render when insets arrive rather than being padded natively. The native implementations are untouched here — the deprecated public `SafeAreaView` still uses them. This only moves the internal component onto the prop, so the two can be compared against each other before the native side is removed.
`SafeAreaView` has been deprecated for several releases in favour of `react-native-safe-area-context`, and the previous change moved the core surfaces that cannot use the library onto a JavaScript implementation built on the safe area insets prop. Nothing needs the native component now. Removed: the C++ shadow node, state and component descriptor, the iOS component view, the Android view and view manager, the codegen spec and every registration of them, plus the long-deprecated public export from `index.js`, the Flow and TypeScript surfaces, the deprecated type stubs, the Babel lazy import list and the ESLint import map. The private component drops its `_INTERNAL_DO_NOT_USE` suffix now that it is the only one left. This is a breaking change for anyone still importing `SafeAreaView` from `react-native`; the replacement is `react-native-safe-area-context`, or the `experimental_onSafeAreaInsetsChange` prop directly.
janicduplessis
force-pushed
the
safe-area/6-remove-native-safe-area-view
branch
from
September 21, 2026 18:23
3a4a9bb to
9c8782f
Compare
This branch has not been deployed
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:
SafeAreaViewhas been deprecated for several releases in favour ofreact-native-safe-area-context, and #58112 moved the core surfaces that cannot use the library onto a JavaScript implementation built on the safe area insets prop. Nothing needs the native component now.Removed: the C++ shadow node, state and component descriptor, the iOS component view, the Android view and view manager, the codegen spec and every registration of them, plus the long-deprecated public export from
index.js, the Flow and TypeScript surfaces, the deprecated type stubs, the Babel lazy-import list and the ESLint import map. The private component drops its_INTERNAL_DO_NOT_USEsuffix now that it is the only one left.This is a breaking change for anyone still importing
SafeAreaViewfromreact-native. The replacements arereact-native-safe-area-contextor, for a view that only needs its own insets, theexperimental_onSafeAreaInsetsChangeprop. Reverting means restoring roughly 800 deleted lines, so it is worth deciding deliberately rather than as a side effect of the rest of the stack — which is why it is last and separate.Stacked on #58112. This PR's diff includes the ones below it until they merge; review the top commit.
Changelog:
[GENERAL] [BREAKING] - Remove the deprecated
SafeAreaViewcomponentTest Plan:
RNTester builds and runs on an iPhone 17 Pro simulator with the component gone: LogBox, the element inspector and
InputAccessoryViewstill clear the system UI, driven by the JavaScript implementation.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. The display-phase event beat this builds on landed as #58530.This PR's own change, without the ones below it: 54 files.
👉 4. #58113 — Remove the native SafeAreaView and the deprecated public export