Repository navigation
[iOS][New Arch] Legacy interop unmounts children by index against a drifted order — bulk keyed swaps detach the wrong subviews (crash + ghost views) #57872
Description
Description
On the New Architecture, RCTLegacyViewManagerInteropComponentView unmounts a legacy component's children by position, not by the child the mutation names:
// React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
if (_adapter && index < _adapter.paperView.reactSubviews.count) {
[_adapter.paperView removeReactSubview:_adapter.paperView.reactSubviews[index]];
} else {
[_viewsToBeUnmounted addObject:childComponentView];
}
}During bulk keyed replacement of a legacy parent's children, the index model behind these remove instructions drifts from the paper view's actual reactSubviews order, so reactSubviews[index] resolves to the wrong subview. Depending on how far the drift goes, the result is either:
- an
NSRangeExceptioncrash when a later insert lands beyond the (over-shrunk) array — e.g.-[__NSArrayM insertObject:atIndex:]: index 11 beyond bounds [0 .. 9]inside the legacy view'sinsertReactSubview:atIndex:, or - silent wrong-victim unmounts: children React kept get detached, and children React removed stay alive as ghosts.
The deferred branch has a second defect: it queues the interop wrapper (childComponentView) and later passes it straight to removeReactSubview:. A legacy parent tracks the unwrapped paper view it originally received, so removal by wrapper identity is a silent no-op (this is the failure mode described in react-native-maps #5080).
Both defects are still present on main as of this report.
Evidence (native trace, run twice with identical results)
Instrumented insertReactSubview: / removeReactSubview: of AIRGoogleMap (react-native-maps 1.20.1, a legacy interop component) with pointer-level NSLogs, in an app whose map renders one keyed <Marker> list plus two trailing conditional <Marker> siblings (a device dot and a vehicle pin).
Scenario: the keyed list goes 4 → 83 → 4 (a provider filter applied, cleared, re-applied).
- Mount (6 children): markers arrive unwrapped, inserts at
idx == count, all consistent. - Grow 4 → 83: 79 inserts arrive at
idx6..84 — i.e. appended after the two trailing siblings, although JSX order places the keyed list before them. The mutation index model and the JSX child order have already diverged; nothing is visually wrong yet. - Shrink 83 → 4: 79 remove instructions arrive. Six of them resolve to the wrong views:
- all 6 originally-mounted markers (4 kept keyed children + both trailing siblings) get removed — React never unmounted them;
- 6 markers that React did unmount never receive a remove, and stay attached with their Fabric child (the icon content) stripped: invisible ghosts.
Trace tallies for step 3: map removes: 79 (count correct, victims wrong), mount-time markers removed: 6/6, ghosts never removed: 6, with zero out-of-bounds inserts. On the unpatched library this same scenario also produced the NSRangeException crash above.
Downstream issue reports that match this defect: react-native-maps #5080, #5217, #5345.
Suggested fix
Remove the child the mutation names, by identity, unwrapping interop wrappers — the paper-era semantic:
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
UIView *target = childComponentView;
if ([childComponentView isKindOfClass:[RCTLegacyViewManagerInteropComponentView class]]) {
UIView *content = ((RCTLegacyViewManagerInteropComponentView *)childComponentView).contentView;
if (content) {
target = content;
}
}
if (_adapter) {
[_adapter.paperView removeReactSubview:target];
} else {
[_viewsToBeUnmounted addObject:childComponentView];
}
}…and the same unwrapping in the _viewsToBeUnmounted replay inside finalizeUpdates:. Identity removal is order-insensitive, so it stays correct however the index model drifts.
Caveat: we could not runtime-verify this patch in our app because Expo SDK 54 links React Native core as a prebuilt XCFramework (RCT_USE_PREBUILT_RNCORE), so the source edit never compiles. The analysis above is from reading the shipped source plus the trace. We worked around the bug at the app level by keying all markers to a per-result-set generation so every swap is a full remount — full replacement never mis-targets, which corroborates that partial swaps are the trigger.
Steps to reproduce
- New Architecture iOS app with a legacy interop component that intercepts children (react-native-maps
MapViewis the readily available one). - Render a keyed list of ~dozens of
<Marker>children followed by two conditionally-rendered<Marker>siblings. - Replace the keyed list with a much larger set (keep a few keys), then replace it back with the small set in a later commit.
- Observe: markers that should remain (including the trailing siblings) disappear from the map; markers that should be gone remain mounted with their content stripped. With enough churn,
NSRangeExceptionin the legacy view'sinsertReactSubview:atIndex:.
React Native Version
0.81.5 (defect verified present in main source at time of filing)
Affected Platforms
Runtime - iOS (New Architecture, legacy view manager interop)
Environment
- Expo SDK 54,
newArchEnabled: true, Hermes - react-native-maps 1.20.1 (Google provider) as the legacy interop component
- Reproduced on the iOS 26.3 simulator (iPhone 17 Pro)
Activity
react-native-bot commentedon Aug 10, 2026
Warning
Missing reproducer: We could not detect a reproducible example in your issue report. Reproducers are mandatory and we can accept only one of those as a valid reproducer:
You can read more about about it on our website: How to report a bug.
richstimson commentedon Aug 24, 2026
Map markers, circles and overlays silently disappear on the New Architecture, SDK 54 (iOS)
TL;DR — if you hit this via react-native-maps, see Start here immediately below: on
bare RN, upgrade the library; on Expo SDK 54 you are pinned to the affected version and the
ready-to-apply diff is the way out. Evidence, reproducer and verification follow after.
Start here — if you are hitting this in react-native-maps (check your Expo SDK first)
Stating this up front because for most people it is a better answer than
patching React Native: react-native-maps ships real Fabric component views
from 1.21.0 (Apr 2025). From that version the library is no longer a legacy
component under the New Architecture, so it never goes through
RCTLegacyViewManagerInteropComponentViewand this bug cannot reach it. We wereon 1.20.1 (Nov 2024) — the last release before that landed.
If you are on bare React Native, upgrading
react-native-mapsto 1.21.0+ isthe fix. Stop here; you do not need the patch.
If you are on Expo, the version is not yours to choose. Expo pins native
module versions per SDK in
bundledNativeModules.json, and SDK 54 pinsreact-native-mapsto exactly 1.20.1 — the one release that is vulnerable.expo install --checkreports "Dependencies are up to date", because by Expo'sdefinition it is. So on SDK 54 you are pinned to the affected version by the
SDK itself, and the RN patch below is the practical way out.
I did try upgrading anyway, and would not recommend it: 1.21.0+ splits its
codegen into a second podspec (
react-native-maps-generated) that Expo'sautolinking does not register, so
pod installfails with "Unable to find aspecification for react-native-maps-generated". It only proceeds after
hand-editing the
Podfile, which trades a small, verified patch for anunsupported native integration. Reverted.
SDK 55 looks like the clean exit. It pins
react-native-maps1.27.2(so do SDK 56 and 57) — well past 1.21.0, therefore Fabric-native, therefore
immune, and it retires the patch entirely. I have verified the pin by unpacking
expo@55.0.29'sbundledNativeModules.json, but I have not yet run SDK 55,so treat "immune on 55" as a strong expectation rather than something I have
personally confirmed on a device.
So, roughly:
react-native-mapsto 1.21.0+None of this makes the interop defect less real. It affects any legacy view
manager that intercepts its children; react-native-maps is just the most visible
casualty, and the one with a version-based escape route. Anyone else hitting it
through a different legacy component has only the RN fix to wait for — which is
why it is still worth fixing upstream.
The diff, verified and ready to apply
Posting this inline and free for anyone to take to a PR. No attribution
needed; the diagnosis and the fix are @rmprosser's — this is only their
suggested change written out and confirmed working.
Against
react-native@0.81.5,packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm:- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index { - if (_adapter && index < _adapter.paperView.reactSubviews.count) { - [_adapter.paperView removeReactSubview:_adapter.paperView.reactSubviews[index]]; + // Remove the child the mutation NAMES, by identity, rather than whatever + // currently sits at `index`: Fabric's index model drifts from a legacy + // parent's own reactSubviews order, so index-based removal detaches the + // wrong subview. Identity removal is order-insensitive. + UIView *target = childComponentView; + if ([childComponentView isKindOfClass:[RCTLegacyViewManagerInteropComponentView class]]) { + UIView *content = ((RCTLegacyViewManagerInteropComponentView *)childComponentView).contentView; + if (content) { + target = content; + } + } + if (_adapter) { + [_adapter.paperView removeReactSubview:target]; } else { [_viewsToBeUnmounted addObject:childComponentView]; } }…and the same unwrapping in the deferred replay inside
finalizeUpdates:,which otherwise hands a legacy parent an interop wrapper it never tracked:
for (UIView *view in _viewsToBeUnmounted) { - [_adapter.paperView removeReactSubview:view]; + UIView *unmountTarget = view; + if ([view isKindOfClass:[RCTLegacyViewManagerInteropComponentView class]]) { + UIView *content = ((RCTLegacyViewManagerInteropComponentView *)view).contentView; + if (content) { + unmountTarget = content; + } + } + [_adapter.paperView removeReactSubview:unmountTarget]; }Both hunks are needed: the first fixes the wrong-victim unmount, the second
fixes the silent no-op on the deferred path.
Searchable symptom terms, since this issue's title describes the mechanism
rather than what people actually see: react-native-maps markers vanish,
pins disappear, Circle/Polygon/Polyline not showing, overlays
disappear after zoom or pan, markers gone after zooming out, map
children never come back, works on the Old Architecture but breaks with
newArchEnabled: trueon Expo SDK 54 / RN 0.81.Independent confirmation of this bug, plus the runtime verification the report
says it was blocked on, and the exact diff that fixes it — free to take as-is.
I hit this on Expo SDK 54 (RN 0.81.5) with react-native-maps 1.20.1 and arrived
at the same two defects and the same fix by reading the source, before finding
this issue.
Simple reproducer
No crash and no bulk keyed swap required — the silent wrong-victim unmount reproduces with three children and one small camera change.
The general shape: any with a short-lived child sandwiched between long-lived ones. "Short-lived" means React unmounts and remounts it as the region changes — typically because its key is derived from the region. The long-lived children just have to persist across that change.
To be clear about provenance: the failure, the before/after verification and the patch result below are all from our production app, on a physical device. The snippet immediately below is a minimal illustration of that same child structure, written to show the shape in the fewest possible lines — I have not run this exact file standalone, so treat it as a description of the pattern rather than a turnkey repro.
Zoom out by any amount — 1.25x is enough, there is no magnitude threshold. The short-lived child is removed from the middle of the list, the mutation indices drift, and the removals land on stable-a and the instead. Those two detach; the short-lived child repaints correctly.
They do not come back. Returning the camera to the byte-identical starting region does not restore them — they were genuinely removed from MapKit, which is what rules out any timing or paint explanation.
We hit this via Supercluster (cluster ids change per zoom, so the cluster markers are the short-lived group), but nothing about clustering is required: viewport-filtered markers, or any region-derived list, produce the same drift.
Key properties, all consistent with the index-drift mechanism:
region does not restore them — a real native removal, not a paint glitch.
AIRMapCircleorAIRMapMarker; it is the sharedinsertReactSubview:/removeReactSubview:path.markers keeps reporting the marker as rendered throughout the failure, which
is what makes this so hard to debug from JS: the JS oracle says the marker is
present while the screen shows nothing. (We instrumented the marker path
specifically; the circle is a plain conditional render, but it disappears and
returns in lockstep with the marker.)
newArchEnabledoff (RN 0.79.6): no repro. Turning the New Architecture onis the only variable — which rules the map library out, since it is pinned
identically on both.
Runtime verification of the suggested fix
The report notes it could not runtime-verify the patch because Expo SDK 54
links RN core as a prebuilt XCFramework. **This project builds RN core from source, so the patch compiles and runs here.
Worth being precise about why, because it is luck rather than configuration:
our
ios/Podfileis checked in and was generated before Expo introduced theprebuilt-core switch, so it has no
RCT_USE_PREBUILT_RNCORElogic in it at alland has never been regenerated. A current SDK 54 template gets the prebuilt
default.
For anyone wanting to reproduce that setup: the switch itself is one line in
ios/Podfile.properties.json—{ "ios.buildReactNativeFromSource": "true" }— but be warned that flipping it on a clean SDK 54 app is not necessarily
enough. I tried exactly that on a scratch app with identical versions
(Expo 54, RN 0.81.5, react-native-maps 1.20.1) to build a standalone
reproducer, and it fails to compile under Xcode 26:
RN 0.81's bundled
fmtdoes not build with Xcode 26's clang, which ispresumably part of why the prebuilt XCFramework exists in the first place. So
"just build from source" is not a trivial suggestion on a current toolchain —
the original report hitting the prebuilt wall is the normal case, not an
unusual one.
Applied exactly as suggested — identity-based removal with wrapper unwrapping
in
unmountChildComponentView:, plus the same unwrapping in the_viewsToBeUnmountedreplay insidefinalizeUpdates:— via patch-package:It fixes it. Verified on a physical iPhone 13 (iOS 26.3.1), screenshots
rather than logs, against the same reproduction that failed before the patch:
animateToRegion+Circleprop change)The second row is the one that matters most: before the patch, returning the
camera to the byte-identical starting region never brought the children back,
because they had been genuinely removed from MapKit. After it, they survive the
whole ladder.
For anyone matching this against their own screenshots: in the broken state the
map still renders normally — base tiles, labels, and the short-lived clustered
markers all repaint correctly at every zoom level. Only the long-lived children
are missing, with no error, no warning, and no crash. That is what makes it read
as "the marker just isn't there" rather than as a rendering fault.
Both defects were load-bearing in this app: the first causes the wrong-victim
unmount, and the second matters because the deferred branch passes the interop
wrapper to
removeReactSubview:, which a legacy parent never tracked.Note on the downstream reports
All three downstream reports are in the react-native-maps repo, and all
three are closed while the defect is still live on RN
main:— "All subviews are not being removed properly when running new architecture"
— "[Expo SDK 52] App crashes with
NSInvalidArgumentException" (41 comments)— "MapView crash when dynamic sub views are added or removed" (22 comments)
The symptom surfaces in a different repo from its cause, so it keeps getting
closed as a maps problem. Worth linking this issue from those threads once it
lands.
react-native-bot commentedon Sep 19, 2026
This issue is waiting for author's feedback since 24 days. Please provide the requested feedback or this will be closed in 7 days.
react-native-bot commentedon Oct 13, 2026
This issue is waiting for author's feedback since 24 days. Please provide the requested feedback or this will be closed in 7 days.
habovh commentedon Oct 15, 2026
This issue is likely going to be fixed by #59005, I believe it should not be closed until then.