From 1755ea50c47da2149a20007582d11d66020f91db Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Wed, 15 Apr 2026 07:16:47 -0700 Subject: [PATCH] Schedule React revision merge to happen before dispatching events (#56449) Summary: Changelog: [ANDROID][FIXED] Schedule React revision merge to happen on `DISPATCH_UI` choreographer queue, before dispatching events Before the introduction of branching, it was possible to handle synchronous events on the same frame they were dispatched. Introduction of branching broke that because merge was scheduled using `runOnUIThread`, which turned out to be after the `DISPATCH_UI` choreographer phase, thus after the dispatch of events. Reviewed By: rubennorte Differential Revision: D100966623 --- .../react/fabric/FabricUIManager.java | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 74204078debd..64d9655cba59 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -101,6 +101,7 @@ import java.util.Map; import java.util.Queue; import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -192,6 +193,13 @@ public class FabricUIManager @ThreadConfined(UI) private final Set mSynchronousEvents = new HashSet<>(); + /** + * Queue of surface IDs that need their React revision merged. Drained during doFrame so that + * synchronous events dispatched by the merge are processed in the same frame. + */ + private final ConcurrentLinkedQueue mPendingReactRevisionMerges = + new ConcurrentLinkedQueue<>(); + /** * This is used to keep track of whether or not the FabricUIManager has been destroyed. Once the * Catalyst instance is being destroyed, we should cease all operation here. @@ -956,13 +964,7 @@ private void scheduleReactRevisionMerge(int surfaceId) { mBinding.mergeReactRevision(surfaceId); } } else { - UiThreadUtil.runOnUiThread( - () -> { - FabricUIManagerBinding binding = mBinding; - if (binding != null) { - binding.mergeReactRevision(surfaceId); - } - }); + mPendingReactRevisionMerges.add(surfaceId); } } @@ -1558,6 +1560,18 @@ public void doFrameGuarded(long frameTimeNanos) { return; } + // Drain pending React revision merges first so that animations, + // preallocation, and mount items operate against the latest revision. + if (ReactNativeFeatureFlags.enableFabricCommitBranching()) { + FabricUIManagerBinding binding = mBinding; + if (binding != null) { + Integer mergeSurfaceId; + while ((mergeSurfaceId = mPendingReactRevisionMerges.poll()) != null) { + binding.mergeReactRevision(mergeSurfaceId); + } + } + } + // Drive any animations from C++. // There is a race condition here between getting/setting // `mDriveCxxAnimations` which shouldn't matter; it's safe to call