mirror of
https://github.com/react/react-native.git
synced 2026-09-28 13:23:09 +08:00
Fix differ creating or deleting a view that a nested (un)flattening moves (#58648)
Summary: Fixes https://github.com/react/react-native/issues/58647 When a view flattens in the same commit in which its parent unflattens, and one of its children has a negative `zIndex`, the differ creates that child again while it's still mounted, or deletes it even though it only moves. On iOS this crashes in `RCTComponentViewRegistry` (Debug) or with a SIGSEGV in `RCTMountingManager` (Release). The `zIndex` sorts the child before the view that contains it, and the nested recursion then matches it through a different `ShadowViewNodePair`, so `inOtherTree()` stayed false on the candidate. I made the final create/delete loop in `calculateShadowViewMutationsFlattener` also skip candidates whose tag the recursion recorded in the sub-visited map. I also fixed `unvisitedRecursiveChildPairs` storing a pointer to a loop-local copy. ## Changelog: [GENERAL] [FIXED] - Fix the differ creating a mounted view again, or deleting a moved view, when a child with a negative `zIndex` moves in a nested flatten/unflatten Pull Request resolved: https://github.com/react/react-native/pull/58648 Test Plan: The [reproducer](https://github.com/pawicao/rn-differ-zindex-flatten-repro) from https://github.com/react/react-native/issues/58647 no longer crashes on iOS with React Native built from source with this change; without it, it crashes on the first swap. Reviewed By: christophpurrer Differential Revision: D121619877 Pulled By: javache fbshipit-source-id: d0e692c8645e5cedff9fee0e88133e18809fd3c7
This commit is contained in:
committed by
meta-codesync[bot]
parent
53bf98bf40
commit
361bc240e4
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+67
@@ -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(
|
||||
<View nativeID="Q" style={{opacity: 0.5}}>
|
||||
<View style={opacityOnGrandparent ? {opacity: 0.5} : null}>
|
||||
<View style={opacityOnGrandparent ? null : {opacity: 0.5}}>
|
||||
<View nativeID="A" />
|
||||
<View nativeID="K" style={{zIndex: -1}} />
|
||||
<View nativeID="B" />
|
||||
</View>
|
||||
</View>
|
||||
</View>,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const expectedOutput = (
|
||||
<rn-view nativeID="Q">
|
||||
<rn-view>
|
||||
<rn-view key="0" nativeID="K" />
|
||||
<rn-view key="1" nativeID="A" />
|
||||
<rn-view key="2" nativeID="B" />
|
||||
</rn-view>
|
||||
</rn-view>
|
||||
);
|
||||
|
||||
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', () => {
|
||||
|
||||
Reference in New Issue
Block a user