Skip to content

Commit e83e491

Browse files
habovhclaude
andcommitted
Fix LegacyViewManagerInterop unmounting the wrong child of a legacy view
Summary: `RCTLegacyViewManagerInteropComponentView` removed a child of the hosted legacy (Paper) view with `removeReactSubview:reactSubviews[index]`, using the Fabric index of the child as a position in the Paper view's `reactSubviews`. The two differ while a mount is queued in `_viewsToBeMounted`, so the wrong child was removed (a child that should be gone stayed attached, "ghost view") and the child named by the mutation was left attached. When the removal was deferred instead (`index >= reactSubviews.count`), it only ran in `finalizeUpdates:`, after the same transaction had already deleted the child. The deleted view was recycled while it still had a superview, which fails `RCTAssert(view.superview == nil, "Attempt to recycle a mounted view")` in debug builds. It also queued the interop wrapper rather than the content view that `mountChildComponentView:index:` inserted, so a child that is itself an interop view was never removed. The child named by the mutation is now looked up: a child whose mount is still queued is dropped from the queue, any other is removed from the Paper view immediately (as the view that was inserted), so it has no superview when the transaction deletes it. Changelog: [IOS] [FIXED] - LegacyViewManagerInterop removes the child named by the unmount mutation, not the one at its index in the legacy view Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent 1c6e0c7 commit e83e491

1 file changed

Lines changed: 23 additions & 3 deletions

File tree

‎packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,30 @@ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childCompone
195195

196196
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
197197
{
198-
if (_adapter && index < _adapter.paperView.reactSubviews.count) {
199-
[_adapter.paperView removeReactSubview:_adapter.paperView.reactSubviews[index]];
198+
// `index` is the child's position among the Fabric children. It is not its position in the paper view's
199+
// `reactSubviews` while mounts are queued in `_viewsToBeMounted`, as those are only inserted in `finalizeUpdates`.
200+
// The child named by the mutation is therefore looked up instead of trusting `index`.
201+
202+
// A child whose mount is still queued never reached the paper view: forget the mount.
203+
for (NSDictionary *mountInstruction in [_viewsToBeMounted copy]) {
204+
if (mountInstruction[kRCTLegacyInteropChildComponentKey] == childComponentView) {
205+
[_viewsToBeMounted removeObject:mountInstruction];
206+
return;
207+
}
208+
}
209+
210+
// `mountChildComponentView:index:` inserts the content view of a child that is itself an interop view.
211+
UIView *target = childComponentView;
212+
if ([childComponentView isKindOfClass:[RCTLegacyViewManagerInteropComponentView class]]) {
213+
target = ((RCTLegacyViewManagerInteropComponentView *)childComponentView).contentView ?: childComponentView;
214+
}
215+
216+
if (_adapter && [_adapter.paperView.reactSubviews containsObject:target]) {
217+
// Removed right away: a `Delete` mutation of this child in the same transaction recycles it before
218+
// `finalizeUpdates:` runs, and a recycled view must not have a superview.
219+
[_adapter.paperView removeReactSubview:target];
200220
} else {
201-
[_viewsToBeUnmounted addObject:childComponentView];
221+
[_viewsToBeUnmounted addObject:target];
202222
}
203223
}
204224

0 commit comments

Comments
 (0)