From 5ff59f23de57e92a7e6d8dba7dbc631396a2d997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20B=C5=82oniarz?= Date: Fri, 30 Oct 2026 05:04:42 -0700 Subject: [PATCH] Notify surface-start listeners when an empty surface starts (#59584) Summary: `UIManager::startSurface` runs the callbacks registered with `UIManager::addOnSurfaceStartCallback` after adding the shadow tree. `UIManager::startEmptySurface`, which `SurfaceHandler` uses when a surface has no module name, only added the shadow tree, so those callbacks never ran for such surfaces. The shared animation backend, `ViewTransitionModule` and C++ Animated set up per-surface state in that callback. C++ Animated installs `AnimatedMountingOverrideDelegate` there. Fantom starts all its surfaces without a module name, so none of them saw a surface start in Fantom tests. `startEmptySurface` now notifies the delegate the same way `startSurface` does. ## Changelog: [GENERAL] [FIXED] - Surfaces started without a module name now run the callbacks registered with `UIManager::addOnSurfaceStartCallback` Pull Request resolved: https://github.com/react/react-native/pull/59584 Test Plan: - `SchedulerDelegateInvalidationTest.EmptySurfaceStartRunsSurfaceStartCallbacks`: starting a surface with no module name runs a registered surface-start callback with that surface's shadow tree. It fails without this change. - `yarn fantom packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js` passes. Reviewed By: christophpurrer Differential Revision: D128154376 Pulled By: bartlomiejbloniarz --- .../SchedulerDelegateInvalidationTest.cpp | 18 ++++++++++++++++++ .../react/renderer/uimanager/UIManager.cpp | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/tests/SchedulerDelegateInvalidationTest.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/tests/SchedulerDelegateInvalidationTest.cpp index 9005250d6063..056b774559d0 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/tests/SchedulerDelegateInvalidationTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/tests/SchedulerDelegateInvalidationTest.cpp @@ -589,6 +589,24 @@ TEST_F( EXPECT_EQ(delegate_->shouldRenderTransactionsCount(), 1); } +TEST_F( + SchedulerDelegateInvalidationTest, + EmptySurfaceStartRunsSurfaceStartCallbacks) { + setUp(); + std::vector startedSurfaces; + scheduler_->getUIManager()->addOnSurfaceStartCallback( + [&](const ShadowTree& shadowTree) { + startedSurfaces.push_back(shadowTree.getSurfaceId()); + }); + surfaceHandler_->constraintLayout( + {.layoutDirection = LayoutDirection::LeftToRight}, {}); + + surfaceHandler_->start(); + + EXPECT_EQ(startedSurfaces, std::vector{11}); + surfaceHandler_->stop(); +} + TEST(SchedulerDelegateTest, animatedPropsUseExistingViewUpdatesByDefault) { class PropsDelegate : public RecordingDelegate { public: diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 7eefe4606177..8bdad687b8a3 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -266,7 +266,15 @@ void UIManager::startSurface( void UIManager::startEmptySurface( ShadowTree::Unique&& shadowTree) const noexcept { TraceSection s("UIManager::startEmptySurface"); + auto surfaceId = shadowTree->getSurfaceId(); shadowTreeRegistry_.add(std::move(shadowTree)); + + shadowTreeRegistry_.visit( + surfaceId, [delegate = delegate_](const ShadowTree& shadowTree) { + if (delegate != nullptr) { + delegate->uiManagerDidStartSurface(shadowTree); + } + }); } void UIManager::setSurfaceProps(