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