From 03621fd67b0e88b2d290d4f54fc6bf5dc6f3bb53 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 7 Oct 2026 16:55:00 +0300 Subject: [PATCH] Fix iOS removeClippedSubviews being lost on recycled views Co-Authored-By: Claude Opus 5.5 (1M context) --- .../View/RCTViewComponentView.mm | 4 +++- .../Mounting/RCTViewComponentViewTests.mm | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 5151aeadd696..41d4c978b01b 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -359,7 +359,9 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & // Disable `removeClippedSubviews` when Fabric View Culling is enabled. if (!ReactNativeFeatureFlags::enableViewCulling()) { - if (oldViewProps.removeClippedSubviews != newViewProps.removeClippedSubviews) { + // Compare against the current state, not `_props`: `prepareForRecycle` resets + // `_removeClippedSubviews` but keeps `_props`. + if (_removeClippedSubviews != newViewProps.removeClippedSubviews) { _removeClippedSubviews = newViewProps.removeClippedSubviews; [self _updateRemoveClippedSubviewsState]; } diff --git a/packages/react-native/React/Tests/Mounting/RCTViewComponentViewTests.mm b/packages/react-native/React/Tests/Mounting/RCTViewComponentViewTests.mm index 1a5e58d91cd1..09019e157173 100644 --- a/packages/react-native/React/Tests/Mounting/RCTViewComponentViewTests.mm +++ b/packages/react-native/React/Tests/Mounting/RCTViewComponentViewTests.mm @@ -143,6 +143,26 @@ - (void)testUnmountAfterToggleOffCleansUpReactSubviews XCTAssertNil(child2.superview); } +- (void)testRemoveClippedSubviewsIsKeptOnRecycledView +{ + RCTViewComponentView *view = [RCTViewComponentView new]; + [view updateProps:makeViewProps(true) oldProps:ViewShadowNode::defaultSharedProps()]; + [view prepareForRecycle]; + + // Reused for another view with removeClippedSubviews. Like the mounting layer + // does for an Insert, pass no old props: the view diffs against its own. + [view updateProps:makeViewProps(true) oldProps:nullptr]; + + UIView *child = [UIView new]; + child.frame = CGRectMake(0, 400, 50, 50); + [view mountChildComponentView:(id)child index:0]; + + // Clipping is on, so the child is tracked and left for the clipping pass to attach. + XCTAssertNil(child.superview); + NSMutableArray *reactSubviews = [view valueForKey:@"_reactSubviews"]; + XCTAssertEqual(reactSubviews.count, 1u); +} + #pragma mark - hitTest against non-invertible transforms (#50797) - (void)testHitTestReturnsNilForZeroScaleYView