diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp
index b76a03881be6..41de087d3f80 100644
--- a/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp
+++ b/packages/react-native/ReactCommon/react/renderer/mounting/Differentiator.cpp
@@ -708,9 +708,9 @@ static void calculateShadowViewMutationsFlattener(
auto unvisitedOtherNodesIt =
unvisitedOtherNodes.find(newChild.shadowView.tag);
if (unvisitedOtherNodesIt != unvisitedOtherNodes.end()) {
- auto unvisitedItPair = *unvisitedOtherNodesIt->second;
+ auto* unvisitedItPair = unvisitedOtherNodesIt->second;
unvisitedRecursiveChildPairs.insert(
- {unvisitedItPair.shadowView.tag, &unvisitedItPair});
+ {unvisitedItPair->shadowView.tag, unvisitedItPair});
} else {
unvisitedRecursiveChildPairs.insert(
{newChild.shadowView.tag, &newChild});
@@ -820,6 +820,9 @@ static void calculateShadowViewMutationsFlattener(
// Final step: go through creation/deletion candidates and delete/create
// subtrees if they were never visited during the execution of the above
// loop and recursions.
+ const auto& subVisitedMap = reparentMode == ReparentMode::Flatten
+ ? *subVisitedOldMap
+ : *subVisitedNewMap;
for (auto& deletionCreationCandidatePair : deletionCreationCandidatePairs) {
auto& treeChildPair = *deletionCreationCandidatePair.second;
@@ -828,7 +831,10 @@ static void calculateShadowViewMutationsFlattener(
// already created/deleted and we don't need to do that here.
// It is always the responsibility of the matcher to update subtrees when
// nodes are matched.
- if (treeChildPair.inOtherTree()) {
+ // The recursion can match the node through a different pair instance
+ // (e.g. when zIndex orders it before its parent), so check its tag too.
+ if (treeChildPair.inOtherTree() ||
+ subVisitedMap.contains(treeChildPair.shadowView.tag)) {
continue;
}
diff --git a/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js b/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js
index affe86ef97bb..c260930ad584 100644
--- a/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js
+++ b/packages/react-native/src/private/renderer/mounting/__tests__/Mounting-itest.js
@@ -446,6 +446,73 @@ describe('ViewFlattening', () => {
/>,
);
});
+
+ test('#58647: child with negative zIndex is kept when its parent flattens and its grandparent unflattens', () => {
+ const root = Fantom.createRoot();
+
+ function render(opacityOnGrandparent: boolean) {
+ Fantom.runTask(() => {
+ root.render(
+
+
+
+
+
+
+
+
+ ,
+ );
+ });
+ }
+
+ const expectedOutput = (
+
+
+
+
+
+
+
+ );
+
+ render(false);
+ root.takeMountingManagerLogs();
+
+ render(true);
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Remove {type: "View", parentNativeID: "Q", index: 0, nativeID: (N/A)}',
+ 'Remove {type: "View", parentNativeID: (N/A), index: 2, nativeID: "B"}',
+ 'Remove {type: "View", parentNativeID: (N/A), index: 1, nativeID: "A"}',
+ 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "K"}',
+ 'Delete {type: "View", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: (N/A)}',
+ 'Insert {type: "View", parentNativeID: "Q", index: 0, nativeID: (N/A)}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "K"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 1, nativeID: "A"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 2, nativeID: "B"}',
+ ]);
+ expect(root.getRenderedOutput({props: ['nativeID']}).toJSX()).toEqual(
+ expectedOutput,
+ );
+
+ render(false);
+ expect(root.takeMountingManagerLogs()).toEqual([
+ 'Remove {type: "View", parentNativeID: (N/A), index: 2, nativeID: "B"}',
+ 'Remove {type: "View", parentNativeID: (N/A), index: 1, nativeID: "A"}',
+ 'Remove {type: "View", parentNativeID: (N/A), index: 0, nativeID: "K"}',
+ 'Remove {type: "View", parentNativeID: "Q", index: 0, nativeID: (N/A)}',
+ 'Delete {type: "View", nativeID: (N/A)}',
+ 'Create {type: "View", nativeID: (N/A)}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 0, nativeID: "K"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 1, nativeID: "A"}',
+ 'Insert {type: "View", parentNativeID: (N/A), index: 2, nativeID: "B"}',
+ 'Insert {type: "View", parentNativeID: "Q", index: 0, nativeID: (N/A)}',
+ ]);
+ expect(root.getRenderedOutput({props: ['nativeID']}).toJSX()).toEqual(
+ expectedOutput,
+ );
+ });
});
describe('reconciliation of setNativeProps and React commit', () => {