Skip to content

feat(web): add unstable_disableViewOnWeb to SafeAreaProvider - #750

Open
giaBaoJS wants to merge 1 commit into
appandflow:mainfrom
giaBaoJS:feat/unstable-disable-view-on-web
Open

giaBaoJS wants to merge 1 commit into
appandflow:mainfrom
giaBaoJS:feat/unstable-disable-view-on-web

Conversation

@giaBaoJS

@giaBaoJS giaBaoJS commented Aug 13, 2026

Copy link
Copy Markdown

Fixes #637

Summary

Context

On web SafeAreaProvider always renders a wrapping View, which consumers don't always want - #637 ran into this because ExpoRoot mounts the provider at the root of the app.

@satya164 pointed out that switching to a fragment unconditionally is not safe:

@janicduplessis fyi the provider works as a wrapper view in native stack on web. changing it to a fragment will most likely break things. it can also break things for people using the library in other scenarios. if you're changing to a fragment then please make it do so only via a prop.

and @janicduplessis settled the API:

I agree, we can add an undocumented unstable_disableViewOnWeb or something like that. We can re-evaluate in the next major.

So this PR is exactly that prop and nothing else: opt-in, undocumented, web-only.

Change

src/NativeSafeAreaProvider.web.tsx:125 returns <>{children}</> instead of the View when unstable_disableViewOnWeb is set. The prop is declared on NativeSafeAreaProviderProps and SafeAreaProviderProps; both of those additions are type-only and compile away.

No README or docs entry, per "undocumented". There are no other unstable_ props in the repo, so there was no existing convention to follow.

Behaviour worth your call

Since #737 the web provider measures its own View to report the frame and to clamp insets. With the wrapper gone there is nothing to measure, so metrics fall back to the window. That is not new code - it is the existing canMeasureProviderElement === false path (src/NativeSafeAreaProvider.web.tsx:47-50, fallback values at :65-70), already exercised by the "falls back to window metrics when ResizeObserver is not available" test. For the root-provider case in the issue, window metrics are the right answer anyway.

I deliberately did not try to preserve per-view measurement without a view - that would need a different design (e.g. measuring the parent element) and is more than an unstable escape hatch should carry. style is likewise ignored when the prop is set, since there is no element to apply it to.

The same now applies to the ref added in #775: with no View there is nothing for setRef to receive, so a consumer's ref stays null while the prop is set. All three are called out in the prop's doc comment, and the two ref tests below pin the behaviour in both directions.

The diff stays out of the measurement effect entirely (only the props destructure and the return statement change), so it should compose cleanly with anything else in flight in that file.

Test Plan

Tests

Six tests added to the existing jsdom suite in src/__tests__/NativeSafeAreaProvider.web-test.tsx:

  • wraps children in a view by default - asserts the rendered DOM is <div><span id="child"></span></div>, i.e. the wrapper is still there when the prop is not set.
  • renders children without a wrapping element when enabled - asserts the DOM is <span id="child"></span>, no wrapper.
  • attaches ref to the wrapping view by default - the ref from feat: expose ref to the underlying View #775 is still populated when the prop is not set.
  • leaves ref null when enabled, since there is no view to attach - the documented consequence of dropping the wrapper.
  • reports window insets and frame when enabled - with getBoundingClientRect mocked to a small rect, metrics still come out as the window insets/frame, proving the fallback path is taken rather than something else being measured.
  • keeps reporting window metrics on resize when enabled - insets and frame still update on resize with the wrapper gone.

The second and fifth together cover the user-visible claim: no wrapper element in the DOM, and insets still resolve.

Verification

  • yarn validate:jest: 5 suites, 31 tests, 11 snapshots, green. Baseline on main is 25 tests, 11 snapshots.
  • Existing snapshots are byte-identical. src/__tests__/__snapshots__/ is untouched in this diff, so the native render path (RNCSafeAreaProvider) produces exactly the same tree as before.
  • Counterfactual: reverting only src/NativeSafeAreaProvider.web.tsx and re-running the suite fails all four "when enabled" tests (the wrapper <div> reappears, the ref is attached, and metrics come from the mocked element rect instead of the window) and passes both default-behaviour tests, as expected.
  • Native is unaffected: after yarn prepare, the only emitted runtime JS containing unstable_disableViewOnWeb is lib/{module,commonjs}/NativeSafeAreaProvider.web.js. Compiled SafeAreaContext.js contains zero references - the interface addition is erased. ios/, android/, common/, src/specs/, NativeSafeAreaProvider.tsx and NativeSafeAreaProvider.windows.tsx are all unchanged.
  • yarn validate:eslint (0 errors), yarn validate:typescript and yarn format:prettier:check all pass.

Rebase

Rebased onto main at 5.10.0. The only upstream change that touched this file is #775, which added the ref prop; the conflict was the props destructure and the return statement, and both sides are kept. The ref interaction described above and its two tests are the only content added since the first push.

Known risk

You mentioned re-evaluating this in the next major. If a major is close, feel free to park this - it's deliberately small and self-contained so it costs nothing to defer or drop.

On web the provider always renders a wrapping View, which is not always
wanted, for example when the provider is mounted at the root of an app by
ExpoRoot. Add an undocumented, unstable opt out that renders children
directly instead.

Since there is no view to measure when the wrapper is gone, insets and
frame fall back to window values, which is the existing behaviour of the
branch that runs when the provider element cannot be measured. For the
same reason style and the ref added in appandflow#775 have no element to apply to,
so ref stays null while the prop is set.

The prop is web only. The default path and every other platform are
unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Question regarding web SafeAreaProvider View usage

1 participant