Description
On Android with the New Architecture, when a sibling component mounts or unmounts mid-session above a section shaped like:
<ScrollView>
{conditionalCard && <Card />} {/* mounts later, shifting everything below */}
…
<View style={{ marginBottom: 24 }}> {/* layout-only wrapper → FLATTENED */}
<Text>Section title</Text>
<View style={{ backgroundColor: '#eee', {/* background → real native view */}
borderRadius: 16, margin: 16, padding: 24 }}>
<Text>Empty state text</Text>
</View>
</View>
</ScrollView>
…the background-bearing inner View keeps its pre-shift native frame while every surrounding text node receives its correct new frame. Because the stranded view draws later in child order, it paints as a "ghost" box on top of whatever component now occupies its old position, and stays there for the rest of the process lifetime (only a full app restart — fresh native views — clears it). Yoga/shadow layout is correct throughout; only the native mount is stale.
The bug is silent data-corruption-style: the visually covered component looks broken, while the actually-broken view is a sibling section further down, making it very hard to attribute.
Evidence (native hierarchy vs shadow tree)
Captured with the ghost on screen, same instant:
Accessibility / shadow tree (all correct):
Text "Budget Overview" screen y≈3139 ✓
Text "No budgets yet…" screen y≈3320 ✓
ViewGroup NetWorth card y 2752..3079 ✓
Native hierarchy (adb exec-out dumpsys activity top), same instant:
[ 90] ReactViewGroup y=2205..2692 h=487 ← CashFlow card (the newly mounted sibling)
[109] ReactViewGroup y=2752..3079 h=327 ← NetWorth card ✓ correct
[117] ReactTextView y=3139..3200 ← "Budget Overview" ✓ correct
[118] ReactViewGroup y=2693..2963 960x270 ← background box ✗ STALE
[119] ReactTextView y=3320..3430 ← "No budgets yet…" ✓ correct
[118]'s correct frame is y=3240..3510 (confirmed after the fix below). Its stale frame is exactly CashFlowCard height (487) + its 60px margin = 547px above the correct one — i.e. its frame from before the sibling mounted. Child order is correct; only the frame update was dropped. Note the box is invisible to accessibility tooling (no a11y node for a background-only view), so a11y-based inspection reports a perfectly healthy screen.
Crash variant
Adding collapsable={false} to the layout-only wrapper (making it a real native view) converts the silent ghost into a hard crash on the same trigger (sibling unmount in this case):
com.facebook.react.bridge.RetryableMountingLayerException: Unable to get view…
at com.facebook.react.fabric.mounting.SurfaceMountingManager.getVi…
at com.facebook.react.fabric.mounting.SurfaceMountingManager.updat…
at com.facebook.react.fabric.mounting.mountitems.IntBufferBatchMou…
at com.facebook.react.fabric.mounting.mountitems.MountItemDispatcher.executeO…
…
Same underlying mounting-diff bookkeeping, surfacing loudly instead of silently — which suggests the differ is emitting mutations against view tags the mounting layer doesn't have when the flattening state around this subtree changes.
Steps to reproduce
- ScrollView (bug reproduces with plain core
ScrollView) containing, in order: several sibling cards, a conditionally rendered card (initially null), then a section shaped as above (flattened layout-only wrapper → background box + texts below it).
- Cold start with the conditional card absent.
- Trigger a state change that mounts the conditional card (in our app: navigate to a form screen, save a record, navigate back; the section list re-renders and the card mounts).
- Scroll to the section below. The background box renders at its pre-mount position, overlapping the sibling above; its texts render at the correct positions.
Reproduced identically on a Pixel 3a emulator (API 35) and a Galaxy S24 Ultra (API 36), debug and release-ish (debugOptimized) builds.
Ruled out (all tested against the same reproduction)
- Reanimated involvement: no
entering/layout animations anywhere in the affected subtree; also reproduced with Reanimated feature flags at defaults.
react-native-screens: reproduces on 4.25.2 and 4.26.2.
- Scroll container: reproduces with
react-native-keyboard-controller's scroll view and with core ScrollView; with and without stickyHeaderIndices.
boxShadow, BlurView siblings, freezeOnBlur: removed, still reproduces.
enableViewCulling is false (default) in this RN version.
Workaround
Remove the flattenable wrapper (return a fragment; move the wrapper's margin onto the background box). With the background box as a direct child of the scroll content, its layout updates apply correctly and the ghost is gone (verified at the native layer: box frame matches shadow tree after repeated mount/unmount cycles). collapsable={false} is not a viable workaround — see crash variant.
Environment
- react-native: 0.86.0 (New Architecture / Fabric, Hermes)
- Platform: Android only (API 35 emulator + API 36 physical device); iOS unaffected
- Expo SDK 57 (dev client,
expo run:android); react: 19.2.3; React Compiler enabled
npx react-native info available on request; happy to build a standalone repro app if useful
Description
On Android with the New Architecture, when a sibling component mounts or unmounts mid-session above a section shaped like:
…the background-bearing inner
Viewkeeps its pre-shift native frame while every surrounding text node receives its correct new frame. Because the stranded view draws later in child order, it paints as a "ghost" box on top of whatever component now occupies its old position, and stays there for the rest of the process lifetime (only a full app restart — fresh native views — clears it). Yoga/shadow layout is correct throughout; only the native mount is stale.The bug is silent data-corruption-style: the visually covered component looks broken, while the actually-broken view is a sibling section further down, making it very hard to attribute.
Evidence (native hierarchy vs shadow tree)
Captured with the ghost on screen, same instant:
Accessibility / shadow tree (all correct):
Native hierarchy (
adb exec-out dumpsys activity top), same instant:[118]'s correct frame isy=3240..3510(confirmed after the fix below). Its stale frame is exactlyCashFlowCard height (487) + its 60px margin = 547pxabove the correct one — i.e. its frame from before the sibling mounted. Child order is correct; only the frame update was dropped. Note the box is invisible to accessibility tooling (no a11y node for a background-only view), so a11y-based inspection reports a perfectly healthy screen.Crash variant
Adding
collapsable={false}to the layout-only wrapper (making it a real native view) converts the silent ghost into a hard crash on the same trigger (sibling unmount in this case):Same underlying mounting-diff bookkeeping, surfacing loudly instead of silently — which suggests the differ is emitting mutations against view tags the mounting layer doesn't have when the flattening state around this subtree changes.
Steps to reproduce
ScrollView) containing, in order: several sibling cards, a conditionally rendered card (initiallynull), then a section shaped as above (flattened layout-only wrapper → background box + texts below it).Reproduced identically on a Pixel 3a emulator (API 35) and a Galaxy S24 Ultra (API 36), debug and release-ish (
debugOptimized) builds.Ruled out (all tested against the same reproduction)
entering/layoutanimations anywhere in the affected subtree; also reproduced with Reanimated feature flags at defaults.react-native-screens: reproduces on 4.25.2 and 4.26.2.react-native-keyboard-controller's scroll view and with coreScrollView; with and withoutstickyHeaderIndices.boxShadow, BlurView siblings,freezeOnBlur: removed, still reproduces.enableViewCullingisfalse(default) in this RN version.Workaround
Remove the flattenable wrapper (return a fragment; move the wrapper's margin onto the background box). With the background box as a direct child of the scroll content, its layout updates apply correctly and the ghost is gone (verified at the native layer: box frame matches shadow tree after repeated mount/unmount cycles).
collapsable={false}is not a viable workaround — see crash variant.Environment
expo run:android); react: 19.2.3; React Compiler enablednpx react-native infoavailable on request; happy to build a standalone repro app if useful