From bd57c562ea9df05c0b46619062f5647a10ad30ef Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Mon, 13 Apr 2026 05:44:24 -0700 Subject: [PATCH] Prevent double calls to `setMounted` when merging a JS revision Summary: Changelog: [GENERAL][FIXED] Fixed double `setMounted` flag calls with fabric branching enabled This change removes the commit source condition from the call site in `ShadowTree::tryCommit` and moves the logic into updateMountedFlag itself: - Mounted flags are skipped for ReactRevisionMerge commits because they were already set during the React branch commit. Setting them again would double increment the EventEmitter's additive enable counter. - Runtime shadow node references updates condition didn't change, only moved to a single variable. - When neither operation is needed, the function returns early to avoid an unnecessary tree traversal under the DispatchMutex. Differential Revision: D100125886 --- .../react/renderer/mounting/ShadowTree.cpp | 2 +- .../renderer/mounting/updateMountedFlag.cpp | 58 ++++++++++++------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp index 928ddf3ef39d..9666041e47fe 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp @@ -422,7 +422,7 @@ CommitStatus ShadowTree::tryCommit( auto newRevisionNumber = currentRevision_.number + 1; - if (!isReactBranch) { + { std::scoped_lock dispatchLock(EventEmitter::DispatchMutex()); updateMountedFlag( currentRevision_.rootShadowNode->getChildren(), diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/updateMountedFlag.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/updateMountedFlag.cpp index 55ab4b7c4cd0..5c50b8377360 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/updateMountedFlag.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/updateMountedFlag.cpp @@ -29,6 +29,25 @@ void updateMountedFlag( return; } + // Mounted flags shouldn't be updated during the React revision merge + // because they were already set during the React branch commit. Setting them + // again would double-increment the EventEmitter's additive enable counter. + bool shouldUpdateMountedFlag = + commitSource != ShadowTreeCommitSource::ReactRevisionMerge; + + // Runtime shadow node references are updated during the React revision + // commits so that JS can access layout data from the merged tree. + bool shouldUpdateRuntimeReference = + (commitSource == ShadowTreeCommitSource::React && + ReactNativeFeatureFlags::updateRuntimeShadowNodeReferencesOnCommit()) || + (ReactNativeFeatureFlags:: + updateRuntimeShadowNodeReferencesOnCommitThread() && + ShadowNode::getUseRuntimeShadowNodeReferenceUpdateOnThread()); + + if (!shouldUpdateMountedFlag && !shouldUpdateRuntimeReference) { + return; + } + size_t index = 0; // Stage 1: Mount and unmount "updated" children. @@ -47,15 +66,12 @@ void updateMountedFlag( break; } - newChild->setMounted(true); - oldChild->setMounted(false); + if (shouldUpdateMountedFlag) { + newChild->setMounted(true); + oldChild->setMounted(false); + } - if ((commitSource == ShadowTreeCommitSource::React && - ReactNativeFeatureFlags:: - updateRuntimeShadowNodeReferencesOnCommit()) || - (ReactNativeFeatureFlags:: - updateRuntimeShadowNodeReferencesOnCommitThread() && - ShadowNode::getUseRuntimeShadowNodeReferenceUpdateOnThread())) { + if (shouldUpdateRuntimeReference) { newChild->updateRuntimeShadowNodeReference(newChild); } @@ -68,14 +84,12 @@ void updateMountedFlag( // State 2: Mount new children. for (index = lastIndexAfterFirstStage; index < newChildren.size(); index++) { const auto& newChild = newChildren[index]; - newChild->setMounted(true); - - if ((commitSource == ShadowTreeCommitSource::React && - ReactNativeFeatureFlags:: - updateRuntimeShadowNodeReferencesOnCommit()) || - (ReactNativeFeatureFlags:: - updateRuntimeShadowNodeReferencesOnCommitThread() && - ShadowNode::getUseRuntimeShadowNodeReferenceUpdateOnThread())) { + + if (shouldUpdateMountedFlag) { + newChild->setMounted(true); + } + + if (shouldUpdateRuntimeReference) { newChild->updateRuntimeShadowNodeReference(newChild); } @@ -83,10 +97,14 @@ void updateMountedFlag( } // State 3: Unmount old children. - for (index = lastIndexAfterFirstStage; index < oldChildren.size(); index++) { - const auto& oldChild = oldChildren[index]; - oldChild->setMounted(false); - updateMountedFlag(oldChild->getChildren(), {}, commitSource); + if (shouldUpdateMountedFlag) { + for (index = lastIndexAfterFirstStage; index < oldChildren.size(); + index++) { + const auto& oldChild = oldChildren[index]; + oldChild->setMounted(false); + + updateMountedFlag(oldChild->getChildren(), {}, commitSource); + } } } } // namespace facebook::react