From 78929bd44809a6c9d73dcd44f80012875654a879 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 13 Jul 2026 14:09:51 +0200 Subject: [PATCH 01/39] feat: add new hook shadowTreeDidCommit --- .../react/renderer/mounting/ShadowTree.cpp | 3 +++ .../react/renderer/mounting/ShadowTreeDelegate.h | 11 +++++++++++ .../react/renderer/uimanager/UIManager.cpp | 15 +++++++++++++++ .../react/renderer/uimanager/UIManager.h | 6 ++++++ .../renderer/uimanager/UIManagerCommitHook.h | 11 +++++++++++ 5 files changed, 46 insertions(+) diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp index bdd301f5c3d5..d4f0722b44a7 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp @@ -463,6 +463,9 @@ CommitStatus ShadowTree::tryCommit( } } + delegate_.shadowTreeDidCommit( + *this, newRevision.rootShadowNode, affectedLayoutableNodes); + emitLayoutEvents(affectedLayoutableNodes); if (isReactBranch) { diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTreeDelegate.h b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTreeDelegate.h index 3dde9c5837c8..9bf7cb246b5f 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTreeDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTreeDelegate.h @@ -11,6 +11,7 @@ namespace facebook::react { +class LayoutableShadowNode; class ShadowTree; struct ShadowTreeCommitOptions; @@ -49,6 +50,16 @@ class ShadowTreeDelegate { */ virtual void shadowTreeDidPromoteReactRevision(const ShadowTree &shadowTree) const = 0; + /* + * Called right after a Shadow Tree commits a new tree, reporting the nodes + * whose layout changed in this commit. + */ + virtual void shadowTreeDidCommit( + const ShadowTree& shadowTree, + const RootShadowNode::Shared& rootShadowNode, + const std::vector& affectedLayoutableNodes) + const noexcept {} + virtual ~ShadowTreeDelegate() noexcept = default; }; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index c89e439b9e3b..0c35031aa5db 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -670,6 +670,21 @@ void UIManager::shadowTreeDidPromoteReactRevision( } } +void UIManager::shadowTreeDidCommit( + const ShadowTree& shadowTree, + const RootShadowNode::Shared& rootShadowNode, + const std::vector& affectedLayoutableNodes) + const noexcept { + TraceSection s("UIManager::shadowTreeDidCommit"); + + std::shared_lock lock(commitHookMutex_); + + for (auto* commitHook : commitHooks_) { + commitHook->shadowTreeDidCommit( + shadowTree, rootShadowNode, affectedLayoutableNodes); + } +} + void UIManager::reportMount(SurfaceId surfaceId) const { TraceSection s("UIManager::reportMount"); diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index 657d66e2128f..db5af6e67cdb 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -146,6 +146,12 @@ class UIManager final : public ShadowTreeDelegate { void shadowTreeDidPromoteReactRevision(const ShadowTree &shadowTree) const override; + void shadowTreeDidCommit( + const ShadowTree& shadowTree, + const RootShadowNode::Shared& rootShadowNode, + const std::vector& affectedLayoutableNodes) + const noexcept override; + std::shared_ptr createNode( Tag tag, const std::string &componentName, diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerCommitHook.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerCommitHook.h index 8c34b59ac03f..60f695c2dc7e 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerCommitHook.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerCommitHook.h @@ -11,6 +11,7 @@ namespace facebook::react { +class LayoutableShadowNode; class ShadowTree; struct ShadowTreeCommitOptions; class UIManager; @@ -54,6 +55,16 @@ class UIManagerCommitHook { return newRootShadowNode; } + /* + * Called right after a `ShadowTree` commits a new tree. + * The semantic of the method corresponds to a method of the same name + * from `ShadowTreeDelegate`. + */ + virtual void shadowTreeDidCommit( + const ShadowTree& /*shadowTree*/, + const RootShadowNode::Shared& /*rootShadowNode*/, + const std::vector& /*affectedLayoutableNodes*/) noexcept {} + virtual ~UIManagerCommitHook() noexcept = default; }; From f430d40381599d5d4a9d7fc309289f195e7bbc76 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Tue, 28 Jul 2026 17:49:28 +0200 Subject: [PATCH 02/39] feat: shadowTreeDidCommit cxx api --- scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api | 3 +++ scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api | 3 +++ scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api | 3 +++ scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api | 3 +++ scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api | 3 +++ scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api | 3 +++ scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api | 3 +++ scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api | 3 +++ scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api | 3 +++ 9 files changed, 27 insertions(+) diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 2f1ede61e222..5e0c5e1710e5 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -4751,6 +4751,7 @@ class facebook::react::ShadowTree { class facebook::react::ShadowTreeDelegate { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) const = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const = 0; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const = 0; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const = 0; @@ -5260,6 +5261,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public std::shared_ptr cloneNode(const facebook::react::ShadowNode& shadowNode, const std::shared_ptr>>& children, facebook::react::RawProps rawProps) const; public std::shared_ptr createNode(facebook::react::Tag tag, const std::string& componentName, facebook::react::SurfaceId surfaceId, facebook::react::RawProps props, facebook::react::InstanceHandle::Shared instanceHandle) const; public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTree::CommitOptions& commitOptions) const override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept override; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const override; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const override; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const override; @@ -5331,6 +5333,7 @@ class facebook::react::UIManagerCommitHook { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const facebook::react::RootShadowNode::Unshared& newRootShadowNode) noexcept; public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept = 0; public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const std::vector&) noexcept; public virtual ~UIManagerCommitHook() noexcept = default; } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index e2e2f1d000c3..fa5c7d406e20 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -4562,6 +4562,7 @@ class facebook::react::ShadowTree { class facebook::react::ShadowTreeDelegate { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) const = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const = 0; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const = 0; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const = 0; @@ -5071,6 +5072,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public std::shared_ptr cloneNode(const facebook::react::ShadowNode& shadowNode, const std::shared_ptr>>& children, facebook::react::RawProps rawProps) const; public std::shared_ptr createNode(facebook::react::Tag tag, const std::string& componentName, facebook::react::SurfaceId surfaceId, facebook::react::RawProps props, facebook::react::InstanceHandle::Shared instanceHandle) const; public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTree::CommitOptions& commitOptions) const override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept override; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const override; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const override; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const override; @@ -5142,6 +5144,7 @@ class facebook::react::UIManagerCommitHook { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const facebook::react::RootShadowNode::Unshared& newRootShadowNode) noexcept; public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept = 0; public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const std::vector&) noexcept; public virtual ~UIManagerCommitHook() noexcept = default; } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index d584b9cd4c2b..8693ba41aa57 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -4742,6 +4742,7 @@ class facebook::react::ShadowTree { class facebook::react::ShadowTreeDelegate { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) const = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const = 0; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const = 0; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const = 0; @@ -5251,6 +5252,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public std::shared_ptr cloneNode(const facebook::react::ShadowNode& shadowNode, const std::shared_ptr>>& children, facebook::react::RawProps rawProps) const; public std::shared_ptr createNode(facebook::react::Tag tag, const std::string& componentName, facebook::react::SurfaceId surfaceId, facebook::react::RawProps props, facebook::react::InstanceHandle::Shared instanceHandle) const; public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTree::CommitOptions& commitOptions) const override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept override; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const override; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const override; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const override; @@ -5322,6 +5324,7 @@ class facebook::react::UIManagerCommitHook { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const facebook::react::RootShadowNode::Unshared& newRootShadowNode) noexcept; public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept = 0; public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const std::vector&) noexcept; public virtual ~UIManagerCommitHook() noexcept = default; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 6e4ab04a9d24..d2e0e86c2bef 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -6966,6 +6966,7 @@ class facebook::react::ShadowTree { class facebook::react::ShadowTreeDelegate { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) const = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const = 0; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const = 0; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const = 0; @@ -7453,6 +7454,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public std::shared_ptr cloneNode(const facebook::react::ShadowNode& shadowNode, const std::shared_ptr>>& children, facebook::react::RawProps rawProps) const; public std::shared_ptr createNode(facebook::react::Tag tag, const std::string& componentName, facebook::react::SurfaceId surfaceId, facebook::react::RawProps props, facebook::react::InstanceHandle::Shared instanceHandle) const; public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTree::CommitOptions& commitOptions) const override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept override; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const override; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const override; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const override; @@ -7524,6 +7526,7 @@ class facebook::react::UIManagerCommitHook { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const facebook::react::RootShadowNode::Unshared& newRootShadowNode) noexcept; public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept = 0; public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const std::vector&) noexcept; public virtual ~UIManagerCommitHook() noexcept = default; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 02957e06c0f6..552617059548 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -6805,6 +6805,7 @@ class facebook::react::ShadowTree { class facebook::react::ShadowTreeDelegate { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) const = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const = 0; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const = 0; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const = 0; @@ -7292,6 +7293,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public std::shared_ptr cloneNode(const facebook::react::ShadowNode& shadowNode, const std::shared_ptr>>& children, facebook::react::RawProps rawProps) const; public std::shared_ptr createNode(facebook::react::Tag tag, const std::string& componentName, facebook::react::SurfaceId surfaceId, facebook::react::RawProps props, facebook::react::InstanceHandle::Shared instanceHandle) const; public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTree::CommitOptions& commitOptions) const override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept override; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const override; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const override; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const override; @@ -7363,6 +7365,7 @@ class facebook::react::UIManagerCommitHook { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const facebook::react::RootShadowNode::Unshared& newRootShadowNode) noexcept; public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept = 0; public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const std::vector&) noexcept; public virtual ~UIManagerCommitHook() noexcept = default; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 94953681427d..3b2e12e5c531 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -6957,6 +6957,7 @@ class facebook::react::ShadowTree { class facebook::react::ShadowTreeDelegate { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) const = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const = 0; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const = 0; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const = 0; @@ -7444,6 +7445,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public std::shared_ptr cloneNode(const facebook::react::ShadowNode& shadowNode, const std::shared_ptr>>& children, facebook::react::RawProps rawProps) const; public std::shared_ptr createNode(facebook::react::Tag tag, const std::string& componentName, facebook::react::SurfaceId surfaceId, facebook::react::RawProps props, facebook::react::InstanceHandle::Shared instanceHandle) const; public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTree::CommitOptions& commitOptions) const override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept override; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const override; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const override; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const override; @@ -7515,6 +7517,7 @@ class facebook::react::UIManagerCommitHook { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const facebook::react::RootShadowNode::Unshared& newRootShadowNode) noexcept; public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept = 0; public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const std::vector&) noexcept; public virtual ~UIManagerCommitHook() noexcept = default; } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 199e5d7791f4..72e6aa89141a 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -3311,6 +3311,7 @@ class facebook::react::ShadowTree { class facebook::react::ShadowTreeDelegate { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) const = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const = 0; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const = 0; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const = 0; @@ -3719,6 +3720,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public std::shared_ptr cloneNode(const facebook::react::ShadowNode& shadowNode, const std::shared_ptr>>& children, facebook::react::RawProps rawProps) const; public std::shared_ptr createNode(facebook::react::Tag tag, const std::string& componentName, facebook::react::SurfaceId surfaceId, facebook::react::RawProps props, facebook::react::InstanceHandle::Shared instanceHandle) const; public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTree::CommitOptions& commitOptions) const override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept override; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const override; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const override; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const override; @@ -3790,6 +3792,7 @@ class facebook::react::UIManagerCommitHook { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const facebook::react::RootShadowNode::Unshared& newRootShadowNode) noexcept; public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept = 0; public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const std::vector&) noexcept; public virtual ~UIManagerCommitHook() noexcept = default; } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 1c1f2201f6bc..0a9b2e99513a 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -3162,6 +3162,7 @@ class facebook::react::ShadowTree { class facebook::react::ShadowTreeDelegate { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) const = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const = 0; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const = 0; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const = 0; @@ -3570,6 +3571,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public std::shared_ptr cloneNode(const facebook::react::ShadowNode& shadowNode, const std::shared_ptr>>& children, facebook::react::RawProps rawProps) const; public std::shared_ptr createNode(facebook::react::Tag tag, const std::string& componentName, facebook::react::SurfaceId surfaceId, facebook::react::RawProps props, facebook::react::InstanceHandle::Shared instanceHandle) const; public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTree::CommitOptions& commitOptions) const override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept override; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const override; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const override; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const override; @@ -3641,6 +3643,7 @@ class facebook::react::UIManagerCommitHook { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const facebook::react::RootShadowNode::Unshared& newRootShadowNode) noexcept; public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept = 0; public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const std::vector&) noexcept; public virtual ~UIManagerCommitHook() noexcept = default; } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 573bb02ca90d..deb936d33f7f 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -3302,6 +3302,7 @@ class facebook::react::ShadowTree { class facebook::react::ShadowTreeDelegate { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTreeCommitOptions& commitOptions) const = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const = 0; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const = 0; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const = 0; @@ -3710,6 +3711,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public std::shared_ptr cloneNode(const facebook::react::ShadowNode& shadowNode, const std::shared_ptr>>& children, facebook::react::RawProps rawProps) const; public std::shared_ptr createNode(facebook::react::Tag tag, const std::string& componentName, facebook::react::SurfaceId surfaceId, facebook::react::RawProps props, facebook::react::InstanceHandle::Shared instanceHandle) const; public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& oldRootShadowNode, const facebook::react::RootShadowNode::Unshared& newRootShadowNode, const facebook::react::ShadowTree::CommitOptions& commitOptions) const override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) const noexcept override; public virtual void shadowTreeDidFinishReactCommit(const facebook::react::ShadowTree& shadowTree) const override; public virtual void shadowTreeDidFinishTransaction(std::shared_ptr mountingCoordinator, bool mountSynchronously) const override; public virtual void shadowTreeDidPromoteReactRevision(const facebook::react::ShadowTree& shadowTree) const override; @@ -3781,6 +3783,7 @@ class facebook::react::UIManagerCommitHook { public virtual facebook::react::RootShadowNode::Unshared shadowTreeWillCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const facebook::react::RootShadowNode::Unshared& newRootShadowNode) noexcept; public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept = 0; public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept = 0; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree&, const facebook::react::RootShadowNode::Shared&, const std::vector&) noexcept; public virtual ~UIManagerCommitHook() noexcept = default; } From 93d5d7df8739729511bfb9475bedb61ccaeb3e9a Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 15 Jul 2026 14:14:04 +0200 Subject: [PATCH 03/39] feat: implement LayoutEventEmitter for onLayout events --- .../react/renderer/mounting/ShadowTree.cpp | 22 ----------- .../react/renderer/mounting/ShadowTree.h | 2 - .../react/renderer/scheduler/Scheduler.cpp | 6 +++ .../renderer/uimanager/LayoutEventEmitter.cpp | 38 +++++++++++++++++++ .../renderer/uimanager/LayoutEventEmitter.h | 32 ++++++++++++++++ 5 files changed, 76 insertions(+), 24 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/LayoutEventEmitter.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/LayoutEventEmitter.h diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp index d4f0722b44a7..9920a83cca5f 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -466,8 +465,6 @@ CommitStatus ShadowTree::tryCommit( delegate_.shadowTreeDidCommit( *this, newRevision.rootShadowNode, affectedLayoutableNodes); - emitLayoutEvents(affectedLayoutableNodes); - if (isReactBranch) { scheduleReactRevisionPromotion(); } else if (commitMode == CommitMode::Normal) { @@ -637,25 +634,6 @@ void ShadowTree::commitEmptyTree() const { {/* default commit options */}); } -void ShadowTree::emitLayoutEvents( - std::vector& affectedLayoutableNodes) const { - TraceSection s( - "ShadowTree::emitLayoutEvents", - "affectedLayoutableNodes", - affectedLayoutableNodes.size()); - - for (const auto* layoutableNode : affectedLayoutableNodes) { - if (auto viewProps = - dynamic_cast(layoutableNode->getProps().get())) { - if (viewProps->onLayout) { - static_cast( - *layoutableNode->getEventEmitter()) - .onLayout(layoutableNode->getLayoutMetrics()); - } - } - } -} - void ShadowTree::notifyDelegatesOfUpdates() const { delegate_.shadowTreeDidFinishTransaction(mountingCoordinator_, true); } diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h index b875ccc80e21..f462271b03a6 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h +++ b/packages/react-native/ReactCommon/react/renderer/mounting/ShadowTree.h @@ -161,8 +161,6 @@ class ShadowTree final { void mount(ShadowTreeRevision revision, bool mountSynchronously) const; - void emitLayoutEvents(std::vector &affectedLayoutableNodes) const; - void scheduleReactRevisionPromotion() const; const SurfaceId surfaceId_; diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 9aba200a839b..77f7719dcc00 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -148,6 +149,11 @@ Scheduler::Scheduler( delegate_ = delegate; commitHooks_ = schedulerToolbox.commitHooks; + + // Layout events (`onLayout`) are emitted as a standalone consumer of the + // `shadowTreeDidCommit` commit hook. + commitHooks_.push_back(std::make_shared()); + uiManager_ = uiManager; for (auto& commitHook : commitHooks_) { diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/LayoutEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/LayoutEventEmitter.cpp new file mode 100644 index 000000000000..5cda81e26748 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/LayoutEventEmitter.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "LayoutEventEmitter.h" + +#include +#include +#include + +namespace facebook::react { + +void LayoutEventEmitter::shadowTreeDidCommit( + const ShadowTree& /*shadowTree*/, + const RootShadowNode::Shared& /*rootShadowNode*/, + const std::vector& + affectedLayoutableNodes) noexcept { + TraceSection s( + "LayoutEventEmitter::shadowTreeDidCommit", + "affectedLayoutableNodes", + affectedLayoutableNodes.size()); + + for (const auto* layoutableNode : affectedLayoutableNodes) { + if (auto viewProps = + dynamic_cast(layoutableNode->getProps().get())) { + if (viewProps->onLayout) { + static_cast( + *layoutableNode->getEventEmitter()) + .onLayout(layoutableNode->getLayoutMetrics()); + } + } + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/LayoutEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/uimanager/LayoutEventEmitter.h new file mode 100644 index 000000000000..128e68ab59ba --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/LayoutEventEmitter.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +/* + * Emits layout events (`onLayout`) for the nodes whose layout changed in + * each commit, using the `shadowTreeDidCommit` commit hook as the source of + * layout changes (this logic used to be implemented directly in + * `ShadowTree`). + */ +class LayoutEventEmitter final : public UIManagerCommitHook { + public: + void commitHookWasRegistered(const UIManager& uiManager) noexcept override {} + void commitHookWasUnregistered(const UIManager& uiManager) noexcept override {} + + void shadowTreeDidCommit( + const ShadowTree& shadowTree, + const RootShadowNode::Shared& rootShadowNode, + const std::vector& + affectedLayoutableNodes) noexcept override; +}; + +} // namespace facebook::react From c2dbd04c56ef5934b7319ee40d1e7d5bd56df2e1 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Tue, 28 Jul 2026 17:50:46 +0200 Subject: [PATCH 04/39] feat: LayoutEventEmitter cxx api --- scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api | 6 ++++++ scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api | 6 ++++++ scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api | 6 ++++++ scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api | 6 ++++++ scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api | 6 ++++++ scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api | 6 ++++++ scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api | 6 ++++++ scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api | 6 ++++++ scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api | 6 ++++++ 9 files changed, 54 insertions(+) diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 5e0c5e1710e5..847169a61096 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -3165,6 +3165,12 @@ class facebook::react::LayoutAnimationStatusDelegate { class facebook::react::LayoutConformanceShadowNode : public facebook::react::ConcreteShadowNode { } +class facebook::react::LayoutEventEmitter : public facebook::react::UIManagerCommitHook { + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) noexcept override; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index fa5c7d406e20..a9acf07b0d1b 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -3074,6 +3074,12 @@ class facebook::react::LayoutAnimationStatusDelegate { class facebook::react::LayoutConformanceShadowNode : public facebook::react::ConcreteShadowNode { } +class facebook::react::LayoutEventEmitter : public facebook::react::UIManagerCommitHook { + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) noexcept override; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 8693ba41aa57..de07ea0cf48f 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -3162,6 +3162,12 @@ class facebook::react::LayoutAnimationStatusDelegate { class facebook::react::LayoutConformanceShadowNode : public facebook::react::ConcreteShadowNode { } +class facebook::react::LayoutEventEmitter : public facebook::react::UIManagerCommitHook { + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) noexcept override; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index d2e0e86c2bef..7a03a12c76ca 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -5397,6 +5397,12 @@ class facebook::react::LayoutAnimationStatusDelegate { class facebook::react::LayoutConformanceShadowNode : public facebook::react::ConcreteShadowNode { } +class facebook::react::LayoutEventEmitter : public facebook::react::UIManagerCommitHook { + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) noexcept override; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 552617059548..d74bbebc5e69 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -5321,6 +5321,12 @@ class facebook::react::LayoutAnimationStatusDelegate { class facebook::react::LayoutConformanceShadowNode : public facebook::react::ConcreteShadowNode { } +class facebook::react::LayoutEventEmitter : public facebook::react::UIManagerCommitHook { + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) noexcept override; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 3b2e12e5c531..08adb4c2a01e 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -5394,6 +5394,12 @@ class facebook::react::LayoutAnimationStatusDelegate { class facebook::react::LayoutConformanceShadowNode : public facebook::react::ConcreteShadowNode { } +class facebook::react::LayoutEventEmitter : public facebook::react::UIManagerCommitHook { + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) noexcept override; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 72e6aa89141a..1a9805cb9286 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -2086,6 +2086,12 @@ class facebook::react::LayoutAnimationStatusDelegate { class facebook::react::LayoutConformanceShadowNode : public facebook::react::ConcreteShadowNode { } +class facebook::react::LayoutEventEmitter : public facebook::react::UIManagerCommitHook { + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) noexcept override; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 0a9b2e99513a..4f812f704861 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -2022,6 +2022,12 @@ class facebook::react::LayoutAnimationStatusDelegate { class facebook::react::LayoutConformanceShadowNode : public facebook::react::ConcreteShadowNode { } +class facebook::react::LayoutEventEmitter : public facebook::react::UIManagerCommitHook { + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) noexcept override; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index deb936d33f7f..70a147742c4c 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -2083,6 +2083,12 @@ class facebook::react::LayoutAnimationStatusDelegate { class facebook::react::LayoutConformanceShadowNode : public facebook::react::ConcreteShadowNode { } +class facebook::react::LayoutEventEmitter : public facebook::react::UIManagerCommitHook { + public virtual void commitHookWasRegistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void commitHookWasUnregistered(const facebook::react::UIManager& uiManager) noexcept override; + public virtual void shadowTreeDidCommit(const facebook::react::ShadowTree& shadowTree, const facebook::react::RootShadowNode::Shared& rootShadowNode, const std::vector& affectedLayoutableNodes) noexcept override; +} + class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode { public LayoutableShadowNode(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment); public LayoutableShadowNode(const facebook::react::ShadowNodeFragment& fragment, const facebook::react::ShadowNodeFamily::Shared& family, facebook::react::ShadowNodeTraits traits); From 78116dec40a663c4b162bc24e2c763f2cca58eb0 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 13 Jul 2026 11:34:39 +0200 Subject: [PATCH 05/39] feat: add enableResizeObserverByDefault feature flag --- .../featureflags/ReactNativeFeatureFlags.kt | 8 +- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 +- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 ++- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 ++- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 108 ++++++++++-------- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 +- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../ReactNativeFeatureFlags.config.js | 9 ++ .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 20 files changed, 179 insertions(+), 64 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 4f5d4962e7fc..b783f23064c3 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2544e292c44dabe82ad82a9b08a08f08>> + * @generated SignedSource<<3427e6c4c4fcd8b7039abab75de7c09f>> */ /** @@ -282,6 +282,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun enablePropsUpdateReconciliationAndroid(): Boolean = accessor.enablePropsUpdateReconciliationAndroid() + /** + * Enables the ResizeObserver Web API in React Native. + */ + @JvmStatic + public fun enableResizeObserverByDefault(): Boolean = accessor.enableResizeObserverByDefault() + /** * When enabled, RuntimeScheduler_Modern clears pending tasks and rendering updates before handling an error. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index ba3018f94045..8e3c2d1637f5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3331513151f7b17e3902cceb65aaac1e>> + * @generated SignedSource<<1a46e8aec0fa83ed44004564bd8a0a2d>> */ /** @@ -62,6 +62,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var enableNativeCSSParsingCache: Boolean? = null private var enablePreparedTextLayoutCache: Boolean? = null private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null + private var enableResizeObserverByDefaultCache: Boolean? = null private var enableRuntimeSchedulerQueueClearingOnErrorCache: Boolean? = null private var enableSchedulerDelegateInvalidationCache: Boolean? = null private var enableSwiftUIBasedFiltersCache: Boolean? = null @@ -485,6 +486,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun enableResizeObserverByDefault(): Boolean { + var cached = enableResizeObserverByDefaultCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.enableResizeObserverByDefault() + enableResizeObserverByDefaultCache = cached + } + return cached + } + override fun enableRuntimeSchedulerQueueClearingOnError(): Boolean { var cached = enableRuntimeSchedulerQueueClearingOnErrorCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 3d7cbf5aa096..209d51534c3d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<71e1be630ee2897f3546ccb23db60197>> + * @generated SignedSource<<05482cf4eb87a1b5d887471fab274a19>> */ /** @@ -112,6 +112,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun enablePropsUpdateReconciliationAndroid(): Boolean + @DoNotStrip @JvmStatic public external fun enableResizeObserverByDefault(): Boolean + @DoNotStrip @JvmStatic public external fun enableRuntimeSchedulerQueueClearingOnError(): Boolean @DoNotStrip @JvmStatic public external fun enableSchedulerDelegateInvalidation(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index c305de4c91eb..1a2ab8ef09e8 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<2b1b95954a9308fddd48f2998dada46d>> */ /** @@ -107,6 +107,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun enablePropsUpdateReconciliationAndroid(): Boolean = false + override fun enableResizeObserverByDefault(): Boolean = false + override fun enableRuntimeSchedulerQueueClearingOnError(): Boolean = false override fun enableSchedulerDelegateInvalidation(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index a6fdc330d12c..7f26b6367bcc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<1a62a0c778f72aa89ef4acea182f17c8>> + * @generated SignedSource<<2db3a05998c8fad1b7806d438c264b4c>> */ /** @@ -66,6 +66,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var enableNativeCSSParsingCache: Boolean? = null private var enablePreparedTextLayoutCache: Boolean? = null private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null + private var enableResizeObserverByDefaultCache: Boolean? = null private var enableRuntimeSchedulerQueueClearingOnErrorCache: Boolean? = null private var enableSchedulerDelegateInvalidationCache: Boolean? = null private var enableSwiftUIBasedFiltersCache: Boolean? = null @@ -531,6 +532,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun enableResizeObserverByDefault(): Boolean { + var cached = enableResizeObserverByDefaultCache + if (cached == null) { + cached = currentProvider.enableResizeObserverByDefault() + accessedFeatureFlags.add("enableResizeObserverByDefault") + enableResizeObserverByDefaultCache = cached + } + return cached + } + override fun enableRuntimeSchedulerQueueClearingOnError(): Boolean { var cached = enableRuntimeSchedulerQueueClearingOnErrorCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 8c30f1cf6e25..8d231163233b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3caf8a7b3aea3b40ff888e5f7f81e90d>> + * @generated SignedSource<> */ /** @@ -107,6 +107,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun enablePropsUpdateReconciliationAndroid(): Boolean + @DoNotStrip public fun enableResizeObserverByDefault(): Boolean + @DoNotStrip public fun enableRuntimeSchedulerQueueClearingOnError(): Boolean @DoNotStrip public fun enableSchedulerDelegateInvalidation(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 065217521357..ae5f64042782 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<1e6921044c4f10ecc5a5263b671772a2>> + * @generated SignedSource<<8739597f69da212bca25f02987da450e>> */ /** @@ -291,6 +291,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool enableResizeObserverByDefault() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableResizeObserverByDefault"); + return method(javaProvider_); + } + bool enableRuntimeSchedulerQueueClearingOnError() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableRuntimeSchedulerQueueClearingOnError"); @@ -769,6 +775,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enablePropsUpdateReconciliationAndroid( return ReactNativeFeatureFlags::enablePropsUpdateReconciliationAndroid(); } +bool JReactNativeFeatureFlagsCxxInterop::enableResizeObserverByDefault( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::enableResizeObserverByDefault(); +} + bool JReactNativeFeatureFlagsCxxInterop::enableRuntimeSchedulerQueueClearingOnError( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::enableRuntimeSchedulerQueueClearingOnError(); @@ -1146,6 +1157,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "enablePropsUpdateReconciliationAndroid", JReactNativeFeatureFlagsCxxInterop::enablePropsUpdateReconciliationAndroid), + makeNativeMethod( + "enableResizeObserverByDefault", + JReactNativeFeatureFlagsCxxInterop::enableResizeObserverByDefault), makeNativeMethod( "enableRuntimeSchedulerQueueClearingOnError", JReactNativeFeatureFlagsCxxInterop::enableRuntimeSchedulerQueueClearingOnError), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index 9a67458d88ce..ba9a65c309ae 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<28884240a4d0cedf7e1ecfaf34378f8e>> + * @generated SignedSource<<911cd43520269b1d56c1ddf2dd3ca1dd>> */ /** @@ -156,6 +156,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool enablePropsUpdateReconciliationAndroid( facebook::jni::alias_ref); + static bool enableResizeObserverByDefault( + facebook::jni::alias_ref); + static bool enableRuntimeSchedulerQueueClearingOnError( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index c07b5a4f375d..35a142961ed8 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -194,6 +194,10 @@ bool ReactNativeFeatureFlags::enablePropsUpdateReconciliationAndroid() { return getAccessor().enablePropsUpdateReconciliationAndroid(); } +bool ReactNativeFeatureFlags::enableResizeObserverByDefault() { + return getAccessor().enableResizeObserverByDefault(); +} + bool ReactNativeFeatureFlags::enableRuntimeSchedulerQueueClearingOnError() { return getAccessor().enableRuntimeSchedulerQueueClearingOnError(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index e0892d8ae8af..6fc7d72b5f93 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<214d05a6296f10f2a7ac67a15dc66d1c>> + * @generated SignedSource<<8a4bac5ae633edc1dba3c72db3321f89>> */ /** @@ -249,6 +249,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool enablePropsUpdateReconciliationAndroid(); + /** + * Enables the ResizeObserver Web API in React Native. + */ + RN_EXPORT static bool enableResizeObserverByDefault(); + /** * When enabled, RuntimeScheduler_Modern clears pending tasks and rendering updates before handling an error. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 7c4b4a62f3d1..231af8312321 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -785,6 +785,24 @@ bool ReactNativeFeatureFlagsAccessor::enablePropsUpdateReconciliationAndroid() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::enableResizeObserverByDefault() { + auto flagValue = enableResizeObserverByDefault_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(42, "enableResizeObserverByDefault"); + + flagValue = currentProvider_->enableResizeObserverByDefault(); + enableResizeObserverByDefault_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::enableRuntimeSchedulerQueueClearingOnError() { auto flagValue = enableRuntimeSchedulerQueueClearingOnError_.load(); @@ -794,7 +812,7 @@ bool ReactNativeFeatureFlagsAccessor::enableRuntimeSchedulerQueueClearingOnError // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(42, "enableRuntimeSchedulerQueueClearingOnError"); + markFlagAsAccessed(43, "enableRuntimeSchedulerQueueClearingOnError"); flagValue = currentProvider_->enableRuntimeSchedulerQueueClearingOnError(); enableRuntimeSchedulerQueueClearingOnError_ = flagValue; @@ -812,7 +830,7 @@ bool ReactNativeFeatureFlagsAccessor::enableSchedulerDelegateInvalidation() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(43, "enableSchedulerDelegateInvalidation"); + markFlagAsAccessed(44, "enableSchedulerDelegateInvalidation"); flagValue = currentProvider_->enableSchedulerDelegateInvalidation(); enableSchedulerDelegateInvalidation_ = flagValue; @@ -830,7 +848,7 @@ bool ReactNativeFeatureFlagsAccessor::enableSwiftUIBasedFilters() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(44, "enableSwiftUIBasedFilters"); + markFlagAsAccessed(45, "enableSwiftUIBasedFilters"); flagValue = currentProvider_->enableSwiftUIBasedFilters(); enableSwiftUIBasedFilters_ = flagValue; @@ -848,7 +866,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewCulling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(45, "enableViewCulling"); + markFlagAsAccessed(46, "enableViewCulling"); flagValue = currentProvider_->enableViewCulling(); enableViewCulling_ = flagValue; @@ -866,7 +884,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecycling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(46, "enableViewRecycling"); + markFlagAsAccessed(47, "enableViewRecycling"); flagValue = currentProvider_->enableViewRecycling(); enableViewRecycling_ = flagValue; @@ -884,7 +902,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForImage() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(47, "enableViewRecyclingForImage"); + markFlagAsAccessed(48, "enableViewRecyclingForImage"); flagValue = currentProvider_->enableViewRecyclingForImage(); enableViewRecyclingForImage_ = flagValue; @@ -902,7 +920,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForScrollView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(48, "enableViewRecyclingForScrollView"); + markFlagAsAccessed(49, "enableViewRecyclingForScrollView"); flagValue = currentProvider_->enableViewRecyclingForScrollView(); enableViewRecyclingForScrollView_ = flagValue; @@ -920,7 +938,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForText() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(49, "enableViewRecyclingForText"); + markFlagAsAccessed(50, "enableViewRecyclingForText"); flagValue = currentProvider_->enableViewRecyclingForText(); enableViewRecyclingForText_ = flagValue; @@ -938,7 +956,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(50, "enableViewRecyclingForView"); + markFlagAsAccessed(51, "enableViewRecyclingForView"); flagValue = currentProvider_->enableViewRecyclingForView(); enableViewRecyclingForView_ = flagValue; @@ -956,7 +974,7 @@ bool ReactNativeFeatureFlagsAccessor::enableVirtualViewContainerStateExperimenta // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(51, "enableVirtualViewContainerStateExperimental"); + markFlagAsAccessed(52, "enableVirtualViewContainerStateExperimental"); flagValue = currentProvider_->enableVirtualViewContainerStateExperimental(); enableVirtualViewContainerStateExperimental_ = flagValue; @@ -974,7 +992,7 @@ bool ReactNativeFeatureFlagsAccessor::fixDifferentiatorParentTagForUnflattenCase // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(52, "fixDifferentiatorParentTagForUnflattenCase"); + markFlagAsAccessed(53, "fixDifferentiatorParentTagForUnflattenCase"); flagValue = currentProvider_->fixDifferentiatorParentTagForUnflattenCase(); fixDifferentiatorParentTagForUnflattenCase_ = flagValue; @@ -992,7 +1010,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(53, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); + markFlagAsAccessed(54, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact(); fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue; @@ -1010,7 +1028,7 @@ bool ReactNativeFeatureFlagsAccessor::fixYogaFlexBasisFitContentInMainAxis() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(54, "fixYogaFlexBasisFitContentInMainAxis"); + markFlagAsAccessed(55, "fixYogaFlexBasisFitContentInMainAxis"); flagValue = currentProvider_->fixYogaFlexBasisFitContentInMainAxis(); fixYogaFlexBasisFitContentInMainAxis_ = flagValue; @@ -1028,7 +1046,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxAssertSingleHostState() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(55, "fuseboxAssertSingleHostState"); + markFlagAsAccessed(56, "fuseboxAssertSingleHostState"); flagValue = currentProvider_->fuseboxAssertSingleHostState(); fuseboxAssertSingleHostState_ = flagValue; @@ -1046,7 +1064,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(56, "fuseboxEnabledRelease"); + markFlagAsAccessed(57, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -1064,7 +1082,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxFrameRecordingEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(57, "fuseboxFrameRecordingEnabled"); + markFlagAsAccessed(58, "fuseboxFrameRecordingEnabled"); flagValue = currentProvider_->fuseboxFrameRecordingEnabled(); fuseboxFrameRecordingEnabled_ = flagValue; @@ -1082,7 +1100,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxScreenshotCaptureEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(58, "fuseboxScreenshotCaptureEnabled"); + markFlagAsAccessed(59, "fuseboxScreenshotCaptureEnabled"); flagValue = currentProvider_->fuseboxScreenshotCaptureEnabled(); fuseboxScreenshotCaptureEnabled_ = flagValue; @@ -1100,7 +1118,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxWebSocketEventsEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(59, "fuseboxWebSocketEventsEnabled"); + markFlagAsAccessed(60, "fuseboxWebSocketEventsEnabled"); flagValue = currentProvider_->fuseboxWebSocketEventsEnabled(); fuseboxWebSocketEventsEnabled_ = flagValue; @@ -1118,7 +1136,7 @@ bool ReactNativeFeatureFlagsAccessor::optimizedAnimatedPropUpdates() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(60, "optimizedAnimatedPropUpdates"); + markFlagAsAccessed(61, "optimizedAnimatedPropUpdates"); flagValue = currentProvider_->optimizedAnimatedPropUpdates(); optimizedAnimatedPropUpdates_ = flagValue; @@ -1136,7 +1154,7 @@ bool ReactNativeFeatureFlagsAccessor::overrideBySynchronousMountPropsAtMountingA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(61, "overrideBySynchronousMountPropsAtMountingAndroid"); + markFlagAsAccessed(62, "overrideBySynchronousMountPropsAtMountingAndroid"); flagValue = currentProvider_->overrideBySynchronousMountPropsAtMountingAndroid(); overrideBySynchronousMountPropsAtMountingAndroid_ = flagValue; @@ -1154,7 +1172,7 @@ bool ReactNativeFeatureFlagsAccessor::perfIssuesEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(62, "perfIssuesEnabled"); + markFlagAsAccessed(63, "perfIssuesEnabled"); flagValue = currentProvider_->perfIssuesEnabled(); perfIssuesEnabled_ = flagValue; @@ -1172,7 +1190,7 @@ bool ReactNativeFeatureFlagsAccessor::perfMonitorV2Enabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(63, "perfMonitorV2Enabled"); + markFlagAsAccessed(64, "perfMonitorV2Enabled"); flagValue = currentProvider_->perfMonitorV2Enabled(); perfMonitorV2Enabled_ = flagValue; @@ -1190,7 +1208,7 @@ double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(64, "preparedTextCacheSize"); + markFlagAsAccessed(65, "preparedTextCacheSize"); flagValue = currentProvider_->preparedTextCacheSize(); preparedTextCacheSize_ = flagValue; @@ -1208,7 +1226,7 @@ bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(65, "preventShadowTreeCommitExhaustion"); + markFlagAsAccessed(66, "preventShadowTreeCommitExhaustion"); flagValue = currentProvider_->preventShadowTreeCommitExhaustion(); preventShadowTreeCommitExhaustion_ = flagValue; @@ -1226,7 +1244,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2Android() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(66, "redBoxV2Android"); + markFlagAsAccessed(67, "redBoxV2Android"); flagValue = currentProvider_->redBoxV2Android(); redBoxV2Android_ = flagValue; @@ -1244,7 +1262,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2IOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(67, "redBoxV2IOS"); + markFlagAsAccessed(68, "redBoxV2IOS"); flagValue = currentProvider_->redBoxV2IOS(); redBoxV2IOS_ = flagValue; @@ -1262,7 +1280,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHo // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(68, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(69, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1280,7 +1298,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldTriggerResponderTransferOnScrollAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(70, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1298,7 +1316,7 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(70, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(71, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1316,7 +1334,7 @@ bool ReactNativeFeatureFlagsAccessor::syncAndroidClipBoundsWithOverflow() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(71, "syncAndroidClipBoundsWithOverflow"); + markFlagAsAccessed(72, "syncAndroidClipBoundsWithOverflow"); flagValue = currentProvider_->syncAndroidClipBoundsWithOverflow(); syncAndroidClipBoundsWithOverflow_ = flagValue; @@ -1334,7 +1352,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(72, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(73, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1352,7 +1370,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(73, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(74, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1370,7 +1388,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(74, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(75, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1388,7 +1406,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(75, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(76, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1406,7 +1424,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "useFabricInterop"); + markFlagAsAccessed(77, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1424,7 +1442,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(77, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(78, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1442,7 +1460,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(78, "useNestedScrollViewAndroid"); + markFlagAsAccessed(79, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1460,7 +1478,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(79, "useSharedAnimatedBackend"); + markFlagAsAccessed(80, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1478,7 +1496,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(81, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1496,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "useTurboModuleInterop"); + markFlagAsAccessed(82, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1514,7 +1532,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "viewCullingOutsetRatio"); + markFlagAsAccessed(83, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1532,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "viewTransitionEnabled"); + markFlagAsAccessed(84, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1550,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionUseHardwareBitmapAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "viewTransitionUseHardwareBitmapAndroid"); + markFlagAsAccessed(85, "viewTransitionUseHardwareBitmapAndroid"); flagValue = currentProvider_->viewTransitionUseHardwareBitmapAndroid(); viewTransitionUseHardwareBitmapAndroid_ = flagValue; @@ -1568,7 +1586,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "virtualViewPrerenderRatio"); + markFlagAsAccessed(86, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index ba6470e680cc..5a33c07fb897 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<2173a0f3b450091d881304551d613eea>> */ /** @@ -74,6 +74,7 @@ class ReactNativeFeatureFlagsAccessor { bool enableNativeCSSParsing(); bool enablePreparedTextLayout(); bool enablePropsUpdateReconciliationAndroid(); + bool enableResizeObserverByDefault(); bool enableRuntimeSchedulerQueueClearingOnError(); bool enableSchedulerDelegateInvalidation(); bool enableSwiftUIBasedFilters(); @@ -129,7 +130,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 86> accessedFeatureFlags_; + std::array, 87> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -173,6 +174,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enableNativeCSSParsing_; std::atomic> enablePreparedTextLayout_; std::atomic> enablePropsUpdateReconciliationAndroid_; + std::atomic> enableResizeObserverByDefault_; std::atomic> enableRuntimeSchedulerQueueClearingOnError_; std::atomic> enableSchedulerDelegateInvalidation_; std::atomic> enableSwiftUIBasedFilters_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 0a0301bd02eb..1737f1c6971c 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<7780bed8af80b7e04d5f4f043b69926c>> + * @generated SignedSource<<3a6b54da681357b049b1204780a3be2f>> */ /** @@ -195,6 +195,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool enableResizeObserverByDefault() override { + return false; + } + bool enableRuntimeSchedulerQueueClearingOnError() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index bee929e939c6..6232741a6dcd 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -423,6 +423,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::enablePropsUpdateReconciliationAndroid(); } + bool enableResizeObserverByDefault() override { + auto value = values_["enableResizeObserverByDefault"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableResizeObserverByDefault(); + } + bool enableRuntimeSchedulerQueueClearingOnError() override { auto value = values_["enableRuntimeSchedulerQueueClearingOnError"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 25e465d14f0c..5f0f671a02ec 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<300197171511ac3c3c483df6a732a766>> + * @generated SignedSource<<7b198409ccf5d97a461709e2f0cea7fb>> */ /** @@ -67,6 +67,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool enableNativeCSSParsing() = 0; virtual bool enablePreparedTextLayout() = 0; virtual bool enablePropsUpdateReconciliationAndroid() = 0; + virtual bool enableResizeObserverByDefault() = 0; virtual bool enableRuntimeSchedulerQueueClearingOnError() = 0; virtual bool enableSchedulerDelegateInvalidation() = 0; virtual bool enableSwiftUIBasedFilters() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index c24d710d08fa..e61d869c8269 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<87934a57cb56397f064fea3bb5cde45d>> + * @generated SignedSource<<57df93302a95f3a492d551adf42855e2>> */ /** @@ -254,6 +254,11 @@ bool NativeReactNativeFeatureFlags::enablePropsUpdateReconciliationAndroid( return ReactNativeFeatureFlags::enablePropsUpdateReconciliationAndroid(); } +bool NativeReactNativeFeatureFlags::enableResizeObserverByDefault( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::enableResizeObserverByDefault(); +} + bool NativeReactNativeFeatureFlags::enableRuntimeSchedulerQueueClearingOnError( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::enableRuntimeSchedulerQueueClearingOnError(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 2820d1b49fe7..9ac6b69cca31 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<12af4836b984b6a200176c14de523bce>> + * @generated SignedSource<> */ /** @@ -120,6 +120,8 @@ class NativeReactNativeFeatureFlags bool enablePropsUpdateReconciliationAndroid(jsi::Runtime& runtime); + bool enableResizeObserverByDefault(jsi::Runtime& runtime); + bool enableRuntimeSchedulerQueueClearingOnError(jsi::Runtime& runtime); bool enableSchedulerDelegateInvalidation(jsi::Runtime& runtime); diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 8d615565c86d..347e56d3e636 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -496,6 +496,15 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + enableResizeObserverByDefault: { + defaultValue: false, + metadata: { + description: 'Enables the ResizeObserver Web API in React Native.', + expectedReleaseValue: true, + purpose: 'release', + }, + ossReleaseStage: 'none', + }, enableRuntimeSchedulerQueueClearingOnError: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 710b18fb365f..cc083df7f582 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3710cc3e3fe2aa8847de749f52bd0501>> + * @generated SignedSource<<4d3408ec756988d663ee861ea3530c3a>> * @flow strict * @noformat */ @@ -90,6 +90,7 @@ export type ReactNativeFeatureFlags = Readonly<{ enableNativeCSSParsing: Getter, enablePreparedTextLayout: Getter, enablePropsUpdateReconciliationAndroid: Getter, + enableResizeObserverByDefault: Getter, enableRuntimeSchedulerQueueClearingOnError: Getter, enableSchedulerDelegateInvalidation: Getter, enableSwiftUIBasedFilters: Getter, @@ -373,6 +374,10 @@ export const enablePreparedTextLayout: Getter = createNativeFlagGetter( * When enabled, Android will receive prop updates based on the differences between the last rendered shadow node and the last committed shadow node. */ export const enablePropsUpdateReconciliationAndroid: Getter = createNativeFlagGetter('enablePropsUpdateReconciliationAndroid', false); +/** + * Enables the ResizeObserver Web API in React Native. + */ +export const enableResizeObserverByDefault: Getter = createNativeFlagGetter('enableResizeObserverByDefault', false); /** * When enabled, RuntimeScheduler_Modern clears pending tasks and rendering updates before handling an error. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 1cbf0c31e06e..952f4f106f5e 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<780c06b6c19f2352cb7896ffeab75475>> + * @generated SignedSource<<52993654dfef57eda460ccb13b18b0c1>> * @flow strict * @noformat */ @@ -67,6 +67,7 @@ export interface Spec extends TurboModule { readonly enableNativeCSSParsing?: () => boolean; readonly enablePreparedTextLayout?: () => boolean; readonly enablePropsUpdateReconciliationAndroid?: () => boolean; + readonly enableResizeObserverByDefault?: () => boolean; readonly enableRuntimeSchedulerQueueClearingOnError?: () => boolean; readonly enableSchedulerDelegateInvalidation?: () => boolean; readonly enableSwiftUIBasedFilters?: () => boolean; From 5e095cc918481b3b2f363faa6711e304ea1d028d Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 13 Jul 2026 11:42:37 +0200 Subject: [PATCH 06/39] feat: ResizeObserver stub implementation --- .../ReactAndroid/src/main/jni/CMakeLists.txt | 3 + .../ReactCommon/React-Fabric.podspec | 6 + .../renderer/observers/resize/CMakeLists.txt | 25 +++ .../observers/resize/ResizeObserver.cpp | 27 +++ .../observers/resize/ResizeObserver.h | 74 +++++++ .../resize/ResizeObserverManager.cpp | 186 ++++++++++++++++++ .../observers/resize/ResizeObserverManager.h | 78 ++++++++ 7 files changed, 399 insertions(+) create mode 100644 packages/react-native/ReactCommon/react/renderer/observers/resize/CMakeLists.txt create mode 100644 packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h create mode 100644 packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h diff --git a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt index 00bbc586170d..39b5e31dab86 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt @@ -118,6 +118,7 @@ add_react_common_subdir(react/renderer/leakchecker) add_react_common_subdir(react/renderer/observers/events) add_react_common_subdir(react/renderer/observers/intersection) add_react_common_subdir(react/renderer/observers/mutation) +add_react_common_subdir(react/renderer/observers/resize) add_react_common_subdir(react/renderer/textlayoutmanager) add_react_common_subdir(react/utils) add_react_common_subdir(react/bridging) @@ -226,6 +227,7 @@ add_library(reactnative $ $ $ + $ $ $ $ @@ -328,6 +330,7 @@ target_include_directories(reactnative $ $ $ + $ $ $ $ diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index 183c039786eb..29f225f8b9e6 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -200,6 +200,12 @@ Pod::Spec.new do |s| sss.exclude_files = "react/renderer/observers/mutation/tests" sss.header_dir = "react/renderer/observers/mutation" end + + ss.subspec "resize" do |sss| + sss.source_files = podspec_sources("react/renderer/observers/resize/**/*.{m,mm,cpp,h}", "react/renderer/observers/resize/**/*.h") + sss.exclude_files = "react/renderer/observers/resize/tests" + sss.header_dir = "react/renderer/observers/resize" + end end s.subspec "telemetry" do |ss| diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/observers/resize/CMakeLists.txt new file mode 100644 index 000000000000..a38b110e7604 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/CMakeLists.txt @@ -0,0 +1,25 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +file(GLOB react_renderer_observers_resize_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_renderer_observers_resize OBJECT ${react_renderer_observers_resize_SRC}) + +target_include_directories(react_renderer_observers_resize PUBLIC ${REACT_COMMON_DIR}) + +target_link_libraries(react_renderer_observers_resize + react_cxxreact + react_bridging + react_renderer_core + react_renderer_graphics + react_renderer_mounting + react_renderer_uimanager +) +target_compile_reactnative_options(react_renderer_observers_resize PRIVATE) +target_compile_options(react_renderer_observers_resize PRIVATE -Wpedantic) diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp new file mode 100644 index 000000000000..321ce9a53b00 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ResizeObserver.h" +#include + +namespace facebook::react { + +ResizeObserver::ResizeObserver( + ResizeObserverObserverId resizeObserverId, + ShadowNodeFamily::Shared targetShadowNodeFamily, + ResizeObserverBoxOptions boxOptions) + : resizeObserverId_(resizeObserverId), + targetShadowNodeFamily_(std::move(targetShadowNodeFamily)), + boxOptions_(boxOptions) {} + +std::optional ResizeObserver::updateStateIfNeeded( + const RootShadowNode& /*rootShadowNode*/) { + // TODO(ResizeObserver): implement + return std::nullopt; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h new file mode 100644 index 000000000000..849abce00a5b --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +using ResizeObserverObserverId = int32_t; + +// Corresponds to the `box` option of `ResizeObserver#observe`. +// https://w3c.github.io/csswg-drafts/resize-observer/#resize-observer-box-options +enum class ResizeObserverBoxOptions { ContentBox, BorderBox }; + +struct ResizeObserverEntry { + ResizeObserverObserverId resizeObserverId; + ShadowNodeFamily::Shared shadowNodeFamily; + Rect contentRect; + Size borderBoxSize; + Size contentBoxSize; + + bool sameShadowNodeFamily( + const ShadowNodeFamily& otherShadowNodeFamily) const { + return std::addressof(*shadowNodeFamily) == + std::addressof(otherShadowNodeFamily); + } +}; + +class ResizeObserver { + public: + ResizeObserver( + ResizeObserverObserverId resizeObserverId, + ShadowNodeFamily::Shared targetShadowNodeFamily, + ResizeObserverBoxOptions boxOptions); + + // Partially equivalent to + // https://w3c.github.io/csswg-drafts/resize-observer/#broadcast-active-resize-observations + std::optional updateStateIfNeeded( + const RootShadowNode& rootShadowNode); + + ResizeObserverObserverId getResizeObserverId() const { + return resizeObserverId_; + } + + ShadowNodeFamily::Shared getTargetShadowNodeFamily() const { + return targetShadowNodeFamily_; + } + + ResizeObserverBoxOptions getBoxOptions() const { + return boxOptions_; + } + + private: + ResizeObserverObserverId resizeObserverId_; + ShadowNodeFamily::Shared targetShadowNodeFamily_; + ResizeObserverBoxOptions boxOptions_; + + // Tracks the "last reported size" for this observation, per the + // ResizeObserver spec. + // https://w3c.github.io/csswg-drafts/resize-observer/#resize-observer-last-reported-size-slot + std::optional lastReportedSize_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp new file mode 100644 index 000000000000..40a0df514e31 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp @@ -0,0 +1,186 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ResizeObserverManager.h" +#include +#include +#include +#include "ResizeObserver.h" + +namespace facebook::react { + +ResizeObserverManager::ResizeObserverManager() = default; + +void ResizeObserverManager::observe( + ResizeObserverObserverId resizeObserverId, + const ShadowNodeFamily::Shared& shadowNodeFamily, + ResizeObserverBoxOptions boxOptions, + UIManager& /*uiManager*/) { + TraceSection s("ResizeObserverManager::observe"); + + auto surfaceId = shadowNodeFamily->getSurfaceId(); + + std::unique_lock lock(observersMutex_); + + auto& observers = observersBySurfaceId_[surfaceId]; + + observers.emplace_back( + std::make_unique( + resizeObserverId, shadowNodeFamily, boxOptions)); +} + +void ResizeObserverManager::unobserve( + ResizeObserverObserverId resizeObserverId, + const ShadowNodeFamily::Shared& shadowNodeFamily) { + TraceSection s("ResizeObserverManager::unobserve"); + + std::unique_lock lock(observersMutex_); + + auto surfaceId = shadowNodeFamily->getSurfaceId(); + + auto observersIt = observersBySurfaceId_.find(surfaceId); + if (observersIt == observersBySurfaceId_.end()) { + return; + } + + auto& observers = observersIt->second; + + observers.erase( + std::remove_if( + observers.begin(), + observers.end(), + [resizeObserverId, &shadowNodeFamily](const auto& observer) { + return observer->getResizeObserverId() == resizeObserverId && + observer->getTargetShadowNodeFamily() == shadowNodeFamily; + }), + observers.end()); + + if (observers.empty()) { + observersBySurfaceId_.erase(surfaceId); + } +} + +void ResizeObserverManager::connect( + UIManager& uiManager, + std::function notifyResizeObserversFunction) { + TraceSection s("ResizeObserverManager::connect"); + + // Fail-safe in case the caller doesn't guarantee consistency. + if (commitHookRegistered_) { + return; + } + + notifyResizeObserversFunction_ = std::move(notifyResizeObserversFunction); + + uiManager.registerCommitHook(*this); + commitHookRegistered_ = true; +} + +void ResizeObserverManager::disconnect(UIManager& uiManager) { + TraceSection s("ResizeObserverManager::disconnect"); + + // Fail-safe in case the caller doesn't guarantee consistency. + if (!commitHookRegistered_) { + return; + } + + uiManager.unregisterCommitHook(*this); + + notifyResizeObserversFunction_ = nullptr; + commitHookRegistered_ = false; +} + +std::vector ResizeObserverManager::takeRecords() { + std::unique_lock lock(pendingEntriesMutex_); + + notifiedResizeObservers_ = false; + + std::vector entries; + pendingEntries_.swap(entries); + return entries; +} + +#pragma mark - UIManagerCommitHook + +void ResizeObserverManager::commitHookWasRegistered( + const UIManager& uiManager) noexcept {} +void ResizeObserverManager::commitHookWasUnregistered( + const UIManager& uiManager) noexcept {} + +RootShadowNode::Unshared ResizeObserverManager::shadowTreeWillCommit( + const ShadowTree& shadowTree, + const RootShadowNode::Shared& oldRootShadowNode, + const RootShadowNode::Unshared& newRootShadowNode, + const ShadowTree::CommitOptions& commitOptions) noexcept { + if (commitOptions.source == ShadowTree::CommitSource::React) { + runResizeObservations(shadowTree, *oldRootShadowNode, *newRootShadowNode); + } + return newRootShadowNode; +} + +#pragma mark - Private methods + +void ResizeObserverManager::runResizeObservations( + const ShadowTree& shadowTree, + const RootShadowNode& /*oldRootShadowNode*/, + const RootShadowNode& newRootShadowNode) { + TraceSection s("ResizeObserverManager::runResizeObservations"); + + auto surfaceId = shadowTree.getSurfaceId(); + + std::vector entries; + + { + std::unique_lock lock(observersMutex_); + + auto observersIt = observersBySurfaceId_.find(surfaceId); + if (observersIt == observersBySurfaceId_.end()) { + return; + } + + auto& observers = observersIt->second; + for (auto& observer : observers) { + // TODO(ResizeObserver): implement + auto entry = observer->updateStateIfNeeded(newRootShadowNode); + if (entry) { + entries.push_back(std::move(entry).value()); + } + } + } + + { + std::unique_lock lock(pendingEntriesMutex_); + pendingEntries_.insert( + pendingEntries_.end(), entries.begin(), entries.end()); + } + + notifyObserversIfNecessary(); +} + +void ResizeObserverManager::notifyObserversIfNecessary() { + bool dispatchNotification = false; + + { + std::unique_lock lock(pendingEntriesMutex_); + + if (!pendingEntries_.empty() && !notifiedResizeObservers_) { + notifiedResizeObservers_ = true; + dispatchNotification = true; + } + } + + if (dispatchNotification) { + notifyObservers(); + } +} + +void ResizeObserverManager::notifyObservers() { + TraceSection s("ResizeObserverManager::notifyObservers"); + notifyResizeObserversFunction_(); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h new file mode 100644 index 000000000000..b92830301e87 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ResizeObserver.h" + +namespace facebook::react { + +class ResizeObserverManager final : public UIManagerCommitHook { + public: + ResizeObserverManager(); + + void observe( + ResizeObserverObserverId resizeObserverId, + const ShadowNodeFamily::Shared& shadowNodeFamily, + ResizeObserverBoxOptions boxOptions, + UIManager& uiManager); + + void unobserve( + ResizeObserverObserverId resizeObserverId, + const ShadowNodeFamily::Shared& shadowNodeFamily); + + void connect( + UIManager& uiManager, + std::function notifyResizeObserversFunction); + + void disconnect(UIManager& uiManager); + + std::vector takeRecords(); + +#pragma mark - UIManagerCommitHook + + void commitHookWasRegistered(const UIManager& uiManager) noexcept override; + void commitHookWasUnregistered(const UIManager& uiManager) noexcept override; + + RootShadowNode::Unshared shadowTreeWillCommit( + const ShadowTree& shadowTree, + const RootShadowNode::Shared& oldRootShadowNode, + const RootShadowNode::Unshared& newRootShadowNode, + const ShadowTree::CommitOptions& commitOptions) noexcept override; + + private: + mutable std:: + unordered_map>> + observersBySurfaceId_; + mutable std::mutex observersMutex_; + + std::function notifyResizeObserversFunction_; + bool commitHookRegistered_{}; + + mutable std::vector pendingEntries_; + mutable std::mutex pendingEntriesMutex_; + mutable bool notifiedResizeObservers_{}; + + void runResizeObservations( + const ShadowTree& shadowTree, + const RootShadowNode& oldRootShadowNode, + const RootShadowNode& newRootShadowNode); + + void notifyObserversIfNecessary(); + void notifyObservers(); +}; + +} // namespace facebook::react From 1a054123966ed8e44059a1ed99f6c5966ff09b31 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 13 Jul 2026 11:43:21 +0200 Subject: [PATCH 07/39] feat: JS Web API stubs --- .../webapis/resizeobserver/ResizeObserver.js | 188 ++++++++++++++++++ .../resizeobserver/ResizeObserverEntry.js | 111 +++++++++++ .../resizeobserver/ResizeObserverSize.js | 60 ++++++ .../internals/ResizeObserverManager.js | 105 ++++++++++ .../specs/NativeResizeObserver.js | 43 ++++ 5 files changed, 507 insertions(+) create mode 100644 packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js create mode 100644 packages/react-native/src/private/webapis/resizeobserver/ResizeObserverEntry.js create mode 100644 packages/react-native/src/private/webapis/resizeobserver/ResizeObserverSize.js create mode 100644 packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js create mode 100644 packages/react-native/src/private/webapis/resizeobserver/specs/NativeResizeObserver.js diff --git a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js new file mode 100644 index 000000000000..d62b24bdeaf0 --- /dev/null +++ b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js @@ -0,0 +1,188 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +// flowlint unsafe-getters-setters:off + +import type {ResizeObserverId} from './internals/ResizeObserverManager'; +import type ResizeObserverEntry from './ResizeObserverEntry'; + +import ReactNativeElement from '../dom/nodes/ReactNativeElement'; +import {setPlatformObject} from '../webidl/PlatformObjects'; +import * as ResizeObserverManager from './internals/ResizeObserverManager'; + +export type ResizeObserverCallback = ( + entries: ReadonlyArray, + observer: ResizeObserver, +) => unknown; + +/** + * Corresponds to the `box` option of `ResizeObserver#observe`. + * https://drafts.csswg.org/resize-observer/#resize-observer-box-options + */ +export type ResizeObserverBoxOptions = 'content-box' | 'border-box'; + +export interface ResizeObserverOptions { + box?: ResizeObserverBoxOptions; +} + +/** + * The [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) + * interface of the Resize Observer API reports changes to the dimensions of + * an element's content or border box. + * + * `ResizeObserver` avoids infinite callback loops and cyclic dependencies + * that are often created when resizing via a callback function, as it only + * observes block and inline sizes on the content box or border box, as + * opposed to observing changes in pixels. + * + * You can add/remove multiple target elements to/from a single + * `ResizeObserver` instance. + * + * TODO(ResizeObserver): this is a stub implementation. `observe`, + * `unobserve` and `disconnect` do not yet notify any callback. + */ +export default class ResizeObserver { + _callback: ResizeObserverCallback; + _observationTargets: Map = + new Map(); + _resizeObserverId: ?ResizeObserverId; + + constructor(callback: ResizeObserverCallback): void { + if (callback == null) { + throw new TypeError( + "Failed to construct 'ResizeObserver': 1 argument required, but only 0 present.", + ); + } + + if (typeof callback !== 'function') { + throw new TypeError( + "Failed to construct 'ResizeObserver': parameter 1 is not of type 'Function'.", + ); + } + + this._callback = callback; + } + + /** + * Starts observing the specified `ReactNativeElement`. + * One observer has one set of observation targets, and each target can be + * observed using either the target's content box or its border box (as + * specified by the `box` option). + * To stop observing an element, call `ResizeObserver.unobserve()`. + */ + observe(target: ReactNativeElement, options?: ResizeObserverOptions): void { + if (target == null) { + throw new TypeError( + "Failed to execute 'observe' on 'ResizeObserver': parameter 1 is null or undefined.", + ); + } + + if (!(target instanceof ReactNativeElement)) { + throw new TypeError( + "Failed to execute 'observe' on 'ResizeObserver': parameter 1 is not of type 'ReactNativeElement'.", + ); + } + + const box = normalizeBoxOption(options?.box); + + if (this._observationTargets.has(target)) { + // Re-observing an already observed target just updates its `box` + // option (per spec, this is equivalent to calling `unobserve` first). + this._observationTargets.set(target, box); + return; + } + + ResizeObserverManager.observe({ + resizeObserverId: this._getOrCreateResizeObserverId(), + target, + box, + }); + + this._observationTargets.set(target, box); + } + + /** + * Ends the observing of the specified `ReactNativeElement`. + */ + unobserve(target: ReactNativeElement): void { + if (!(target instanceof ReactNativeElement)) { + throw new TypeError( + "Failed to execute 'unobserve' on 'ResizeObserver': parameter 1 is not of type 'ReactNativeElement'.", + ); + } + + if (!this._observationTargets.has(target)) { + return; + } + + const resizeObserverId = this._resizeObserverId; + if (resizeObserverId == null) { + // This is unexpected if the target is in `_observationTargets`. + console.error( + "Unexpected state in 'ResizeObserver': could not find observer ID to unobserve target.", + ); + return; + } + + ResizeObserverManager.unobserve(resizeObserverId, target); + this._observationTargets.delete(target); + + if (this._observationTargets.size === 0) { + ResizeObserverManager.unregisterObserver(resizeObserverId); + this._resizeObserverId = null; + } + } + + /** + * Unobserves all observed `ReactNativeElement` targets. + */ + disconnect(): void { + for (const target of this._observationTargets.keys()) { + this.unobserve(target); + } + } + + _getOrCreateResizeObserverId(): ResizeObserverId { + let resizeObserverId = this._resizeObserverId; + if (resizeObserverId == null) { + resizeObserverId = ResizeObserverManager.registerObserver( + this, + this._callback, + ); + this._resizeObserverId = resizeObserverId; + } + return resizeObserverId; + } + + // Only for tests + __getObserverID(): ?ResizeObserverId { + return this._resizeObserverId; + } +} + +setPlatformObject(ResizeObserver); + +function normalizeBoxOption( + box: ?ResizeObserverBoxOptions, +): ResizeObserverBoxOptions { + if (box == null) { + return 'content-box'; + } + + if (box !== 'content-box' && box !== 'border-box') { + throw new TypeError( + `Failed to execute 'observe' on 'ResizeObserver': Failed to read the 'box' property from 'ResizeObserverOptions': The provided value '${String( + box, + )}' is not a valid enum value of type ResizeObserverBoxOptions.`, + ); + } + + return box; +} diff --git a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverEntry.js b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverEntry.js new file mode 100644 index 000000000000..7d067b94ea19 --- /dev/null +++ b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverEntry.js @@ -0,0 +1,111 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +// flowlint unsafe-getters-setters:off + +import type ReactNativeElement from '../dom/nodes/ReactNativeElement'; +import type {NativeResizeObserverEntry} from './specs/NativeResizeObserver'; + +import DOMRectReadOnly from '../geometry/DOMRectReadOnly'; +import {setPlatformObject} from '../webidl/PlatformObjects'; +import ResizeObserverSize from './ResizeObserverSize'; + +/** + * The [`ResizeObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) + * interface of the Resize Observer API represents the object passed to the + * `ResizeObserver()` callback function, which allows access to the new + * dimensions of the observed target element after its size has changed. + * + * Note: `devicePixelContentBoxSize` is not supported in React Native. + */ +export default class ResizeObserverEntry { + // We lazily compute all the properties from the raw entry provided by the + // native module, so we avoid unnecessary work. + _nativeEntry: NativeResizeObserverEntry; + // There are cases where this cannot be safely derived from the instance + // handle in the native entry (when the target is detached), so we need to + // keep a reference to it directly. + _target: ReactNativeElement; + + constructor( + nativeEntry: NativeResizeObserverEntry, + target: ReactNativeElement, + ) { + this._nativeEntry = nativeEntry; + this._target = target; + } + + /** + * The `ReactNativeElement` whose size has changed. + */ + get target(): ReactNativeElement { + return this._target; + } + + /** + * Returns a `DOMRectReadOnly` object which, prior to Firefox 57, contained + * the new size of the observed element. + * + * In React Native, this is the content rect of the observed target (the + * bounds of its content box). + */ + get contentRect(): DOMRectReadOnly { + const contentRect = this._nativeEntry.contentRect; + return new DOMRectReadOnly( + contentRect[0], + contentRect[1], + contentRect[2], + contentRect[3], + ); + } + + /** + * An array containing the new border box size of the observed element. + * + * Note: React Native does not support fragments, so this array will + * always contain a single value. + */ + get borderBoxSize(): ReadonlyArray { + const borderBoxSize = this._nativeEntry.borderBoxSize; + return [new ResizeObserverSize(borderBoxSize[0], borderBoxSize[1])]; + } + + /** + * An array containing the new content box size of the observed element. + * + * Note: React Native does not support fragments, so this array will + * always contain a single value. + */ + get contentBoxSize(): ReadonlyArray { + const contentBoxSize = this._nativeEntry.contentBoxSize; + return [new ResizeObserverSize(contentBoxSize[0], contentBoxSize[1])]; + } +} + +setPlatformObject(ResizeObserverEntry); + +export function createResizeObserverEntry( + entry: NativeResizeObserverEntry, + target: ReactNativeElement, +): ResizeObserverEntry { + return new ResizeObserverEntry(entry, target); +} + +export const ResizeObserverEntry_public: typeof ResizeObserverEntry = + /* eslint-disable no-shadow */ + // $FlowExpectedError[incompatible-type] + function ResizeObserverEntry() { + throw new TypeError( + "Failed to construct 'ResizeObserverEntry': Illegal constructor", + ); + }; + +// $FlowExpectedError[prop-missing] +ResizeObserverEntry_public.prototype = ResizeObserverEntry.prototype; diff --git a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverSize.js b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverSize.js new file mode 100644 index 000000000000..3fb961f4e3b9 --- /dev/null +++ b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverSize.js @@ -0,0 +1,60 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +// flowlint unsafe-getters-setters:off + +import {setPlatformObject} from '../webidl/PlatformObjects'; + +/** + * The [`ResizeObserverSize`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverSize) + * interface of the Resize Observer API is used to store the block and inline + * sizes of a box as separate properties. + * + * It is returned by the `contentBoxSize` and `borderBoxSize` properties of + * `ResizeObserverEntry`. + */ +export default class ResizeObserverSize { + _inlineSize: number; + _blockSize: number; + + constructor(inlineSize: number, blockSize: number) { + this._inlineSize = inlineSize; + this._blockSize = blockSize; + } + + /** + * The length of the observed element's border box, in the inline + * dimension. For boxes with a horizontal writing-mode, this is the vertical + * dimension, or height; if the writing-mode is vertical, this is the + * horizontal dimension, or width. + */ + get inlineSize(): number { + return this._inlineSize; + } + + /** + * The length of the observed element's border box, in the block dimension. + * For boxes with a horizontal writing-mode, this is the vertical + * dimension, or height; if the writing-mode is vertical, this is the + * horizontal dimension, or width. + */ + get blockSize(): number { + return this._blockSize; + } +} + +setPlatformObject(ResizeObserverSize); + +export function createResizeObserverSize( + inlineSize: number, + blockSize: number, +): ResizeObserverSize { + return new ResizeObserverSize(inlineSize, blockSize); +} diff --git a/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js b/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js new file mode 100644 index 000000000000..b2254e4fbefb --- /dev/null +++ b/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js @@ -0,0 +1,105 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +/** + * This module handles the communication between the React Native renderer + * and all the resize observers that are currently observing any targets. + * + * In order to reduce the communication between native and JavaScript, + * we register a single notification callback in native, and then we handle + * how to notify each entry to the right resize observer when we receive all + * the notifications together. + * + * TODO(ResizeObserver): this module is a stub. None of the exported + * functions communicate with native yet. + */ + +import type ReactNativeElement from '../../dom/nodes/ReactNativeElement'; +import type ResizeObserver, { + ResizeObserverBoxOptions, + ResizeObserverCallback, +} from '../ResizeObserver'; + +// TODO(ResizeObserver): uncomment once the native module is wired up. +// import NativeResizeObserver from '../specs/NativeResizeObserver'; + +export type ResizeObserverId = number; + +let nextResizeObserverId: ResizeObserverId = 1; + +const registeredResizeObservers: Map< + ResizeObserverId, + {observer: ResizeObserver, callback: ResizeObserverCallback}, +> = new Map(); + +/** + * Registers the given resize observer and returns a unique ID for it, which + * is required to start observing targets. + */ +export function registerObserver( + observer: ResizeObserver, + callback: ResizeObserverCallback, +): ResizeObserverId { + const resizeObserverId = nextResizeObserverId; + nextResizeObserverId++; + registeredResizeObservers.set(resizeObserverId, { + observer, + callback, + }); + return resizeObserverId; +} + +/** + * Unregisters the given resize observer. + * This should only be called when an observer is no longer observing any + * targets. + */ +export function unregisterObserver(resizeObserverId: ResizeObserverId): void { + // TODO(ResizeObserver): implement. Once there are no observers left, this + // should disconnect the native module (mirroring + // `IntersectionObserverManager.unregisterObserver`). + registeredResizeObservers.delete(resizeObserverId); +} + +/** + * Starts observing a target on a specific resize observer. + * If this is the first target being observed, this should also set up the + * centralized notification callback in native. + */ +export function observe({ + resizeObserverId, + target, + box, +}: { + resizeObserverId: ResizeObserverId, + target: ReactNativeElement, + box: ResizeObserverBoxOptions, +}): void { + // TODO(ResizeObserver): implement. +} + +/** + * Instructs the given resize observer to stop observing the specified + * target. + */ +export function unobserve( + resizeObserverId: ResizeObserverId, + target: ReactNativeElement, +): void { + // TODO(ResizeObserver): implement. +} + +// TODO(ResizeObserver): implement a `notifyResizeObservers` function that +// will be called from native when there are `ResizeObserver` entries to +// dispatch. It should read the pending entries from +// `NativeResizeObserver.takeRecords()`, group them by observer, and invoke +// each observer's callback (mirroring +// `IntersectionObserverManager.notifyIntersectionObservers` / +// `doNotifyIntersectionObservers`). diff --git a/packages/react-native/src/private/webapis/resizeobserver/specs/NativeResizeObserver.js b/packages/react-native/src/private/webapis/resizeobserver/specs/NativeResizeObserver.js new file mode 100644 index 000000000000..33640952c8a2 --- /dev/null +++ b/packages/react-native/src/private/webapis/resizeobserver/specs/NativeResizeObserver.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +import type {TurboModule} from '../../../../../Libraries/TurboModule/RCTExport'; + +import * as TurboModuleRegistry from '../../../../../Libraries/TurboModule/TurboModuleRegistry'; + +export type NativeResizeObserverEntry = { + resizeObserverId: number, + targetInstanceHandle: unknown, + contentRect: ReadonlyArray, // It's actually a tuple with x, y, width and height + borderBoxSize: ReadonlyArray, // It's actually a tuple with inlineSize and blockSize + contentBoxSize: ReadonlyArray, // It's actually a tuple with inlineSize and blockSize +}; + +export type NativeResizeObserverObserveOptions = { + resizeObserverId: number, + targetShadowNode: unknown, + // Corresponds to the `box` option of `ResizeObserver#observe`. + box?: ?string, +}; + +export interface Spec extends TurboModule { + readonly observe: (options: NativeResizeObserverObserveOptions) => void; + readonly unobserve: ( + resizeObserverId: number, + targetShadowNode: unknown, + ) => void; + readonly connect: (notifyResizeObserversFunction: () => void) => void; + readonly disconnect: () => void; + readonly takeRecords: () => ReadonlyArray; +} + +export default TurboModuleRegistry.get( + 'NativeResizeObserverCxx', +) as ?Spec; From 362000bc3fb222a306744b5e5ae37413df46cd35 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 13 Jul 2026 11:43:41 +0200 Subject: [PATCH 08/39] feat: ResizeObserver TurboModule --- .../ReactAndroid/src/main/jni/CMakeLists.txt | 3 + .../nativemodule/defaults/CMakeLists.txt | 1 + .../defaults/DefaultTurboModules.cpp | 7 + .../React-defaultsnativemodule.podspec | 1 + .../resizeobserver/CMakeLists.txt | 27 ++++ .../resizeobserver/NativeResizeObserver.cpp | 120 ++++++++++++++++++ .../resizeobserver/NativeResizeObserver.h | 92 ++++++++++++++ .../React-resizeobservernativemodule.podspec | 66 ++++++++++ .../runtime/ReactCxxTurboModuleProvider.cpp | 3 + .../react-native/scripts/react_native_pods.rb | 1 + 10 files changed, 321 insertions(+) create mode 100644 packages/react-native/ReactCommon/react/nativemodule/resizeobserver/CMakeLists.txt create mode 100644 packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp create mode 100644 packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.h create mode 100644 packages/react-native/ReactCommon/react/nativemodule/resizeobserver/React-resizeobservernativemodule.podspec diff --git a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt index 39b5e31dab86..3af99f9c7d50 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt @@ -131,6 +131,7 @@ add_react_common_subdir(react/nativemodule/microtasks) add_react_common_subdir(react/nativemodule/idlecallbacks) add_react_common_subdir(react/nativemodule/intersectionobserver) add_react_common_subdir(react/nativemodule/mutationobserver) +add_react_common_subdir(react/nativemodule/resizeobserver) add_react_common_subdir(react/nativemodule/viewtransition) add_react_common_subdir(react/nativemodule/webperformance) add_react_common_subdir(react/networking) @@ -203,6 +204,7 @@ add_library(reactnative $ $ $ + $ $ $ $ @@ -304,6 +306,7 @@ target_include_directories(reactnative $ $ $ + $ $ $ $ diff --git a/packages/react-native/ReactCommon/react/nativemodule/defaults/CMakeLists.txt b/packages/react-native/ReactCommon/react/nativemodule/defaults/CMakeLists.txt index 8e583a48d748..77820e7020be 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/defaults/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/nativemodule/defaults/CMakeLists.txt @@ -23,6 +23,7 @@ target_link_libraries(react_nativemodule_defaults react_nativemodule_idlecallbacks react_nativemodule_intersectionobserver react_nativemodule_mutationobserver + react_nativemodule_resizeobserver react_nativemodule_viewtransition react_nativemodule_webperformance react_renderer_animated diff --git a/packages/react-native/ReactCommon/react/nativemodule/defaults/DefaultTurboModules.cpp b/packages/react-native/ReactCommon/react/nativemodule/defaults/DefaultTurboModules.cpp index ddc0242c3e66..a5c8e23f02f7 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/defaults/DefaultTurboModules.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/defaults/DefaultTurboModules.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -58,6 +59,12 @@ namespace facebook::react { } } + if (ReactNativeFeatureFlags::enableResizeObserverByDefault()) { + if (name == NativeResizeObserver::kModuleName) { + return std::make_shared(jsInvoker); + } + } + if (ReactNativeFeatureFlags::viewTransitionEnabled()) { if (name == NativeViewTransition::kModuleName) { return std::make_shared(jsInvoker); diff --git a/packages/react-native/ReactCommon/react/nativemodule/defaults/React-defaultsnativemodule.podspec b/packages/react-native/ReactCommon/react/nativemodule/defaults/React-defaultsnativemodule.podspec index e68eb7c530a4..9a0f25c036ba 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/defaults/React-defaultsnativemodule.podspec +++ b/packages/react-native/ReactCommon/react/nativemodule/defaults/React-defaultsnativemodule.podspec @@ -54,6 +54,7 @@ Pod::Spec.new do |s| s.dependency "React-idlecallbacksnativemodule" s.dependency "React-intersectionobservernativemodule" s.dependency "React-mutationobservernativemodule" + s.dependency "React-resizeobservernativemodule" s.dependency "React-viewtransitionnativemodule" s.dependency "React-webperformancenativemodule" s.dependency "React-Fabric/animated" diff --git a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/CMakeLists.txt b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/CMakeLists.txt new file mode 100644 index 000000000000..2f22a7fd5393 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/CMakeLists.txt @@ -0,0 +1,27 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +file(GLOB react_nativemodule_resizeobserver_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_nativemodule_resizeobserver OBJECT ${react_nativemodule_resizeobserver_SRC}) + +target_include_directories(react_nativemodule_resizeobserver PUBLIC ${REACT_COMMON_DIR}) + +target_link_libraries(react_nativemodule_resizeobserver + react_codegen_rncore + react_cxxreact + react_renderer_bridging + react_renderer_core + react_renderer_graphics + react_renderer_observers_resize + react_renderer_uimanager + rrc_view +) +target_compile_reactnative_options(react_nativemodule_resizeobserver PRIVATE) +target_compile_options(react_nativemodule_resizeobserver PRIVATE -Wpedantic -Wno-deprecated-declarations) diff --git a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp new file mode 100644 index 000000000000..b9e7ef269ca3 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "NativeResizeObserver.h" +#include +#include +#include + +#ifdef RN_DISABLE_OSS_PLUGIN_HEADER +#include "Plugins.h" +#endif + +std::shared_ptr +NativeResizeObserverModuleProvider( + std::shared_ptr jsInvoker) { + return std::make_shared( + std::move(jsInvoker)); +} + +namespace facebook::react { + +NativeResizeObserver::NativeResizeObserver( + std::shared_ptr jsInvoker) + : NativeResizeObserverCxxSpec(std::move(jsInvoker)) {} + +void NativeResizeObserver::observe( + jsi::Runtime& runtime, + NativeResizeObserverObserveOptions options) { + auto resizeObserverId = options.resizeObserverId; + auto shadowNode = options.targetShadowNode; + auto shadowNodeFamily = shadowNode->getFamilyShared(); + auto boxOptions = boxOptionsFromOptionalString(options.box); + auto& uiManager = getUIManagerFromRuntime(runtime); + + resizeObserverManager_.observe( + resizeObserverId, shadowNodeFamily, boxOptions, uiManager); +} + +void NativeResizeObserver::unobserve( + jsi::Runtime& /*runtime*/, + NativeResizeObserverResizeObserverId resizeObserverId, + std::shared_ptr targetShadowNode) { + auto shadowNodeFamily = targetShadowNode->getFamilyShared(); + resizeObserverManager_.unobserve(resizeObserverId, shadowNodeFamily); +} + +void NativeResizeObserver::connect( + jsi::Runtime& runtime, + AsyncCallback<> notifyResizeObserversFunction) { + auto& uiManager = getUIManagerFromRuntime(runtime); + resizeObserverManager_.connect( + uiManager, std::move(notifyResizeObserversFunction)); +} + +void NativeResizeObserver::disconnect(jsi::Runtime& runtime) { + auto& uiManager = getUIManagerFromRuntime(runtime); + resizeObserverManager_.disconnect(uiManager); +} + +std::vector NativeResizeObserver::takeRecords( + jsi::Runtime& runtime) { + // TODO(ResizeObserver): this will always return an empty vector until + // ResizeObserver::updateStateIfNeeded is implemented. + auto entries = resizeObserverManager_.takeRecords(); + + std::vector nativeModuleEntries; + nativeModuleEntries.reserve(entries.size()); + + for (const auto& entry : entries) { + nativeModuleEntries.emplace_back( + convertToNativeModuleEntry(entry, runtime)); + } + + return nativeModuleEntries; +} + +NativeResizeObserverEntry NativeResizeObserver::convertToNativeModuleEntry( + const ResizeObserverEntry& entry, + jsi::Runtime& runtime) { + RectAsTuple contentRect = { + entry.contentRect.origin.x, + entry.contentRect.origin.y, + entry.contentRect.size.width, + entry.contentRect.size.height}; + SizeAsTuple borderBoxSize = { + entry.borderBoxSize.width, entry.borderBoxSize.height}; + SizeAsTuple contentBoxSize = { + entry.contentBoxSize.width, entry.contentBoxSize.height}; + + NativeResizeObserverEntry nativeModuleEntry = { + entry.resizeObserverId, + (*entry.shadowNodeFamily).getInstanceHandle(runtime), + contentRect, + borderBoxSize, + contentBoxSize, + }; + + return nativeModuleEntry; +} + +UIManager& NativeResizeObserver::getUIManagerFromRuntime( + jsi::Runtime& runtime) { + return UIManagerBinding::getBinding(runtime)->getUIManager(); +} + +ResizeObserverBoxOptions NativeResizeObserver::boxOptionsFromOptionalString( + const std::optional& box) { + if (box.has_value() && box.value() == "border-box") { + return ResizeObserverBoxOptions::BorderBox; + } + + // "content-box" is the default value. + return ResizeObserverBoxOptions::ContentBox; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.h b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.h new file mode 100644 index 000000000000..10c20b600020 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#if __has_include("FBReactNativeSpecJSI.h") // CocoaPod headers on Apple +#include "FBReactNativeSpecJSI.h" +#else +#include +#endif +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +using NativeResizeObserverResizeObserverId = ResizeObserverObserverId; +using RectAsTuple = std::tuple; +using SizeAsTuple = std::tuple; + +using NativeResizeObserverObserveOptions = + NativeResizeObserverNativeResizeObserverObserveOptions< + // resizeObserverId + NativeResizeObserverResizeObserverId, + // targetShadowNode + std::shared_ptr, + // box + std::optional>; + +template <> +struct Bridging + : NativeResizeObserverNativeResizeObserverObserveOptionsBridging< + NativeResizeObserverObserveOptions> {}; + +using NativeResizeObserverEntry = NativeResizeObserverNativeResizeObserverEntry< + // resizeObserverId + NativeResizeObserverResizeObserverId, + // targetInstanceHandle + jsi::Value, + // contentRect + RectAsTuple, + // borderBoxSize + SizeAsTuple, + // contentBoxSize + SizeAsTuple>; + +template <> +struct Bridging + : NativeResizeObserverNativeResizeObserverEntryBridging< + NativeResizeObserverEntry> {}; + +class NativeResizeObserver + : public NativeResizeObserverCxxSpec { + public: + NativeResizeObserver(std::shared_ptr jsInvoker); + + void observe( + jsi::Runtime& runtime, + NativeResizeObserverObserveOptions options); + + void unobserve( + jsi::Runtime& runtime, + NativeResizeObserverResizeObserverId resizeObserverId, + std::shared_ptr targetShadowNode); + + void connect( + jsi::Runtime& runtime, + AsyncCallback<> notifyResizeObserversFunction); + + void disconnect(jsi::Runtime& runtime); + + std::vector takeRecords(jsi::Runtime& runtime); + + private: + ResizeObserverManager resizeObserverManager_{}; + + static UIManager& getUIManagerFromRuntime(jsi::Runtime& runtime); + static ResizeObserverBoxOptions boxOptionsFromOptionalString( + const std::optional& box); + static NativeResizeObserverEntry convertToNativeModuleEntry( + const ResizeObserverEntry& entry, + jsi::Runtime& runtime); +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/React-resizeobservernativemodule.podspec b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/React-resizeobservernativemodule.podspec new file mode 100644 index 000000000000..db9fb8b6ecc3 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/React-resizeobservernativemodule.podspec @@ -0,0 +1,66 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "..", "..", "..", "..", "package.json"))) +version = package['version'] + +source = { :git => 'https://github.com/facebook/react-native.git' } +if version == '1000.0.0' + # This is an unpublished version, use the latest commit hash of the react-native repo, which we're presumably in. + source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1") +else + source[:tag] = "v#{version}" +end + +header_search_paths = [ + "\"$(PODS_ROOT)/Headers/Private/Yoga\"", +] + +if ENV['USE_FRAMEWORKS'] + header_search_paths << "\"$(PODS_TARGET_SRCROOT)/../../..\"" # this is needed to allow the module access its own files +end + +Pod::Spec.new do |s| + s.name = "React-resizeobservernativemodule" + s.version = version + s.summary = "React Native resize observer native module" + s.homepage = "https://reactnative.dev/" + s.license = package["license"] + s.author = "Meta Platforms, Inc. and its affiliates" + s.platforms = min_supported_versions + s.source = source + s.source_files = podspec_sources("*.{cpp,h}", "*.h") + s.header_dir = "react/nativemodule/resizeobserver" + s.pod_target_xcconfig = { "CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(), + "HEADER_SEARCH_PATHS" => header_search_paths.join(' '), + "OTHER_CFLAGS" => "$(inherited)", + "DEFINES_MODULE" => "YES" } + + if ENV['USE_FRAMEWORKS'] + s.module_name = "resizeobservernativemodule" + s.header_mappings_dir = "../.." + end + + s.dependency "Yoga" + s.dependency "React-jsi" + s.dependency "React-jsiexecutor" + s.dependency "React-cxxreact" + + depend_on_js_engine(s) + add_rn_third_party_dependencies(s) + add_rncore_dependency(s) + + s.dependency "ReactCommon/turbomodule/core" + + s.dependency "React-Fabric" + s.dependency "React-Fabric/bridging" + s.dependency "React-Fabric/observers/resize" + add_dependency(s, "React-RCTFBReactNativeSpec") + add_dependency(s, "React-runtimeexecutor", :additional_framework_paths => ["platform/ios"]) + add_dependency(s, "React-graphics", :additional_framework_paths => ["react/renderer/graphics/platform/ios"]) + +end diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.cpp b/packages/react-native/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.cpp index ab2af66e29e6..04630f69b6a5 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.cpp +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -95,6 +96,8 @@ std::shared_ptr ReactCxxTurboModuleProvider::operator()( return std::make_shared(jsInvoker_); } else if (name == NativeMutationObserver::kModuleName) { return std::make_shared(jsInvoker_); + } else if (name == NativeResizeObserver::kModuleName) { + return std::make_shared(jsInvoker_); } else if (name == NetworkingModule::kModuleName) { return std::make_shared(jsInvoker_, httpClientFactory_); } else if (name == LogBoxModule::kModuleName) { diff --git a/packages/react-native/scripts/react_native_pods.rb b/packages/react-native/scripts/react_native_pods.rb index 3b6d3142b38e..1f684afd7105 100644 --- a/packages/react-native/scripts/react_native_pods.rb +++ b/packages/react-native/scripts/react_native_pods.rb @@ -187,6 +187,7 @@ def use_react_native! ( pod 'React-idlecallbacksnativemodule', :path => "#{prefix}/ReactCommon/react/nativemodule/idlecallbacks" pod 'React-intersectionobservernativemodule', :path => "#{prefix}/ReactCommon/react/nativemodule/intersectionobserver" pod 'React-mutationobservernativemodule', :path => "#{prefix}/ReactCommon/react/nativemodule/mutationobserver" + pod 'React-resizeobservernativemodule', :path => "#{prefix}/ReactCommon/react/nativemodule/resizeobserver" pod 'React-viewtransitionnativemodule', :path => "#{prefix}/ReactCommon/react/nativemodule/viewtransition" pod 'React-webperformancenativemodule', :path => "#{prefix}/ReactCommon/react/nativemodule/webperformance" pod 'React-domnativemodule', :path => "#{prefix}/ReactCommon/react/nativemodule/dom" From 9e2b7dcb4b93fd4d3012febf7512a6ae241f4a40 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 13 Jul 2026 11:44:29 +0200 Subject: [PATCH 09/39] feat: add polyfill behind feature flag --- .../setUpDefaultReactNativeEnvironment.js | 6 +++ .../src/private/setup/setUpResizeObserver.js | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 packages/react-native/src/private/setup/setUpResizeObserver.js diff --git a/packages/react-native/src/private/setup/setUpDefaultReactNativeEnvironment.js b/packages/react-native/src/private/setup/setUpDefaultReactNativeEnvironment.js index 6bd27c4cc3d1..b05bd70cb88c 100644 --- a/packages/react-native/src/private/setup/setUpDefaultReactNativeEnvironment.js +++ b/packages/react-native/src/private/setup/setUpDefaultReactNativeEnvironment.js @@ -53,4 +53,10 @@ export default function setUpDefaultReactNativeEnvironment( ) { require('./setUpMutationObserver').default(); } + + if ( + require('../../../src/private/featureflags/ReactNativeFeatureFlags').enableResizeObserverByDefault() + ) { + require('./setUpResizeObserver').default(); + } } diff --git a/packages/react-native/src/private/setup/setUpResizeObserver.js b/packages/react-native/src/private/setup/setUpResizeObserver.js new file mode 100644 index 000000000000..5f73a73b6947 --- /dev/null +++ b/packages/react-native/src/private/setup/setUpResizeObserver.js @@ -0,0 +1,38 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import {polyfillGlobal} from '../../../Libraries/Utilities/PolyfillFunctions'; + +let initialized = false; + +export default function setUpResizeObserver() { + if (initialized) { + return; + } + + initialized = true; + + polyfillGlobal( + 'ResizeObserver', + () => require('../webapis/resizeobserver/ResizeObserver').default, + ); + + polyfillGlobal( + 'ResizeObserverEntry', + () => + require('../webapis/resizeobserver/ResizeObserverEntry') + .ResizeObserverEntry_public, + ); + + polyfillGlobal( + 'ResizeObserverSize', + () => require('../webapis/resizeobserver/ResizeObserverSize').default, + ); +} From 366e58117e3c425cd796253ef65a9326fda5384c Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Tue, 14 Jul 2026 16:52:39 +0200 Subject: [PATCH 10/39] feat: add basic implementation --- .../observers/resize/ResizeObserver.cpp | 65 ++++++- .../observers/resize/ResizeObserver.h | 7 + .../resize/ResizeObserverManager.cpp | 94 +++++++-- .../observers/resize/ResizeObserverManager.h | 12 +- .../internals/ResizeObserverManager.js | 179 ++++++++++++++++-- 5 files changed, 310 insertions(+), 47 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp index 321ce9a53b00..fe60c1594c6d 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp @@ -6,6 +6,8 @@ */ #include "ResizeObserver.h" +#include +#include #include namespace facebook::react { @@ -18,10 +20,67 @@ ResizeObserver::ResizeObserver( targetShadowNodeFamily_(std::move(targetShadowNodeFamily)), boxOptions_(boxOptions) {} +// Partially equivalent to +// https://w3c.github.io/csswg-drafts/resize-observer/#broadcast-active-resize-observations std::optional ResizeObserver::updateStateIfNeeded( - const RootShadowNode& /*rootShadowNode*/) { - // TODO(ResizeObserver): implement - return std::nullopt; + const RootShadowNode& rootShadowNode) { + auto ancestors = targetShadowNodeFamily_->getAncestors(rootShadowNode); + + auto layoutMetrics = ancestors.empty() + ? EmptyLayoutMetrics + : LayoutableShadowNode::computeRelativeLayoutMetrics( + ancestors, + {.includeTransform = false, .includeViewportOffset = true}); + + if (layoutMetrics == EmptyLayoutMetrics) { + // The target is not (or no longer) part of the tree rooted at + // `rootShadowNode`. Per spec, if the element is later reinserted into the + // tree it must be treated as a brand new observation, so we reset the + // last reported size. + lastReportedSize_.reset(); + return std::nullopt; + } + + auto borderBoxSize = layoutMetrics.frame.size; + + // In RN's `LayoutMetrics`, `contentInsets` represents the combined width of + // border and padding in each direction, which matches the Web's definition + // of the content-box. The size is clamped to zero because a frame can be + // smaller than its insets (e.g. `width` smaller than the sum of the + // horizontal borders), but a content box can never have a negative size. + auto contentFrame = layoutMetrics.getContentFrame(); + Size contentBoxSize{ + .width = std::max(Float{0}, contentFrame.size.width), + .height = std::max(Float{0}, contentFrame.size.height)}; + + // Per spec, `contentRect`'s origin is the offset of the content box from + // the padding box (i.e. the paddings only, excluding borders). + // https://w3c.github.io/csswg-drafts/resize-observer/#dom-resizeobserverentry-contentrect + Rect contentRect{ + .origin = + {.x = layoutMetrics.contentInsets.left - + layoutMetrics.borderWidth.left, + .y = layoutMetrics.contentInsets.top - + layoutMetrics.borderWidth.top}, + .size = contentBoxSize}; + + auto observedSize = boxOptions_ == ResizeObserverBoxOptions::ContentBox + ? contentBoxSize + : borderBoxSize; + + if (lastReportedSize_.has_value() && + lastReportedSize_.value() == observedSize) { + return std::nullopt; + } + + lastReportedSize_ = observedSize; + + return ResizeObserverEntry{ + resizeObserverId_, + targetShadowNodeFamily_, + contentRect, + borderBoxSize, + contentBoxSize}; } } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h index 849abce00a5b..795067dada0f 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h @@ -60,6 +60,13 @@ class ResizeObserver { return boxOptions_; } + // Whether this observer has ever produced an entry (i.e. reported a size + // at least once since being created or since the target was last removed + // from the tree). + bool hasReported() const { + return lastReportedSize_.has_value(); + } + private: ResizeObserverObserverId resizeObserverId_; ShadowNodeFamily::Shared targetShadowNodeFamily_; diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp index 40a0df514e31..44a36951eb25 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp @@ -7,30 +7,70 @@ #include "ResizeObserverManager.h" #include +#include #include +#include #include #include "ResizeObserver.h" namespace facebook::react { +namespace { +RootShadowNode::Shared getRootShadowNode( + SurfaceId surfaceId, + const ShadowTreeRegistry& shadowTreeRegistry) { + RootShadowNode::Shared rootShadowNode = nullptr; + + shadowTreeRegistry.visit(surfaceId, [&](const ShadowTree& shadowTree) { + rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode; + }); + + return rootShadowNode; +} +} // namespace + ResizeObserverManager::ResizeObserverManager() = default; void ResizeObserverManager::observe( ResizeObserverObserverId resizeObserverId, const ShadowNodeFamily::Shared& shadowNodeFamily, ResizeObserverBoxOptions boxOptions, - UIManager& /*uiManager*/) { + UIManager& uiManager) { TraceSection s("ResizeObserverManager::observe"); auto surfaceId = shadowNodeFamily->getSurfaceId(); - std::unique_lock lock(observersMutex_); + // Per spec, `observe` must perform an initial observation synchronously, + // using the shadow tree that is current at the time `observe` is called + // (i.e., independent of the commit hook, which only runs for subsequent + // commits). + RootShadowNode::Shared rootShadowNode = + getRootShadowNode(surfaceId, uiManager.getShadowTreeRegistry()); + + std::optional entry; + + { + std::unique_lock lock(observersMutex_); + + auto& observers = observersBySurfaceId_[surfaceId]; + + observers.emplace_back(std::make_unique( + resizeObserverId, shadowNodeFamily, boxOptions)); - auto& observers = observersBySurfaceId_[surfaceId]; + auto* observer = observers.back().get(); - observers.emplace_back( - std::make_unique( - resizeObserverId, shadowNodeFamily, boxOptions)); + if (rootShadowNode != nullptr) { + entry = observer->updateStateIfNeeded(*rootShadowNode); + } + } + + if (entry) { + { + std::unique_lock lock(pendingEntriesMutex_); + pendingEntries_.push_back(std::move(entry).value()); + } + notifyObserversIfNecessary(); + } } void ResizeObserverManager::unobserve( @@ -111,27 +151,33 @@ void ResizeObserverManager::commitHookWasRegistered( void ResizeObserverManager::commitHookWasUnregistered( const UIManager& uiManager) noexcept {} -RootShadowNode::Unshared ResizeObserverManager::shadowTreeWillCommit( +void ResizeObserverManager::shadowTreeDidCommit( const ShadowTree& shadowTree, - const RootShadowNode::Shared& oldRootShadowNode, - const RootShadowNode::Unshared& newRootShadowNode, - const ShadowTree::CommitOptions& commitOptions) noexcept { - if (commitOptions.source == ShadowTree::CommitSource::React) { - runResizeObservations(shadowTree, *oldRootShadowNode, *newRootShadowNode); - } - return newRootShadowNode; + const RootShadowNode::Shared& rootShadowNode, + const std::vector& + affectedLayoutableNodes) noexcept { + runResizeObservations(shadowTree, *rootShadowNode, affectedLayoutableNodes); } #pragma mark - Private methods void ResizeObserverManager::runResizeObservations( const ShadowTree& shadowTree, - const RootShadowNode& /*oldRootShadowNode*/, - const RootShadowNode& newRootShadowNode) { + const RootShadowNode& rootShadowNode, + const std::vector& affectedLayoutableNodes) { TraceSection s("ResizeObserverManager::runResizeObservations"); auto surfaceId = shadowTree.getSurfaceId(); + // Build a set of the families whose layout actually changed in this + // commit, so we only recompute observers that could possibly have a new + // size. + std::unordered_set affectedFamilies; + affectedFamilies.reserve(affectedLayoutableNodes.size()); + for (const auto* node : affectedLayoutableNodes) { + affectedFamilies.insert(&node->getFamily()); + } + std::vector entries; { @@ -144,8 +190,16 @@ void ResizeObserverManager::runResizeObservations( auto& observers = observersIt->second; for (auto& observer : observers) { - // TODO(ResizeObserver): implement - auto entry = observer->updateStateIfNeeded(newRootShadowNode); + // Only recompute observers whose target was affected by this commit, + // or that have never reported yet (e.g. an initial observation that + // raced with a commit before it could run). + bool wasAffected = affectedFamilies.contains( + observer->getTargetShadowNodeFamily().get()); + if (!wasAffected && observer->hasReported()) { + continue; + } + + auto entry = observer->updateStateIfNeeded(rootShadowNode); if (entry) { entries.push_back(std::move(entry).value()); } @@ -180,7 +234,9 @@ void ResizeObserverManager::notifyObserversIfNecessary() { void ResizeObserverManager::notifyObservers() { TraceSection s("ResizeObserverManager::notifyObservers"); - notifyResizeObserversFunction_(); + if (notifyResizeObserversFunction_) { + notifyResizeObserversFunction_(); + } } } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h index b92830301e87..e75eb2f6909f 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h @@ -47,11 +47,11 @@ class ResizeObserverManager final : public UIManagerCommitHook { void commitHookWasRegistered(const UIManager& uiManager) noexcept override; void commitHookWasUnregistered(const UIManager& uiManager) noexcept override; - RootShadowNode::Unshared shadowTreeWillCommit( + void shadowTreeDidCommit( const ShadowTree& shadowTree, - const RootShadowNode::Shared& oldRootShadowNode, - const RootShadowNode::Unshared& newRootShadowNode, - const ShadowTree::CommitOptions& commitOptions) noexcept override; + const RootShadowNode::Shared& rootShadowNode, + const std::vector& + affectedLayoutableNodes) noexcept override; private: mutable std:: @@ -68,8 +68,8 @@ class ResizeObserverManager final : public UIManagerCommitHook { void runResizeObservations( const ShadowTree& shadowTree, - const RootShadowNode& oldRootShadowNode, - const RootShadowNode& newRootShadowNode); + const RootShadowNode& rootShadowNode, + const std::vector& affectedLayoutableNodes); void notifyObserversIfNecessary(); void notifyObservers(); diff --git a/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js b/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js index b2254e4fbefb..f5879f494e29 100644 --- a/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js +++ b/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js @@ -16,9 +16,6 @@ * we register a single notification callback in native, and then we handle * how to notify each entry to the right resize observer when we receive all * the notifications together. - * - * TODO(ResizeObserver): this module is a stub. None of the exported - * functions communicate with native yet. */ import type ReactNativeElement from '../../dom/nodes/ReactNativeElement'; @@ -26,19 +23,50 @@ import type ResizeObserver, { ResizeObserverBoxOptions, ResizeObserverCallback, } from '../ResizeObserver'; +import type ResizeObserverEntry from '../ResizeObserverEntry'; -// TODO(ResizeObserver): uncomment once the native module is wired up. -// import NativeResizeObserver from '../specs/NativeResizeObserver'; +import {trace} from '../../../../../Libraries/Performance/Systrace'; +import { + getInstanceHandle, + getNativeNodeReference, +} from '../../dom/nodes/internals/NodeInternals'; +import {createResizeObserverEntry} from '../ResizeObserverEntry'; +import NativeResizeObserver from '../specs/NativeResizeObserver'; export type ResizeObserverId = number; let nextResizeObserverId: ResizeObserverId = 1; +let isConnected: boolean = false; const registeredResizeObservers: Map< ResizeObserverId, {observer: ResizeObserver, callback: ResizeObserverCallback}, > = new Map(); +// We need to keep the mapping from instance handles to targets because when +// targets are detached (their components are unmounted), React resets the +// instance handle to prevent memory leaks and it cuts the connection between +// the instance handle and the target. +const instanceHandleToTargetMap: WeakMap = + new WeakMap(); + +function getTargetFromInstanceHandle( + instanceHandle: unknown, +): ?ReactNativeElement { + // $FlowExpectedError[incompatible-type] instanceHandle is typed as mixed but we know it's an object and we need it to be to use it as a key in a WeakMap. + const key: interface {} = instanceHandle; + return instanceHandleToTargetMap.get(key); +} + +function setTargetForInstanceHandle( + instanceHandle: unknown, + target: ReactNativeElement, +): void { + // $FlowExpectedError[incompatible-type] instanceHandle is typed as mixed but we know it's an object and we need it to be to use it as a key in a WeakMap. + const key: interface {} = instanceHandle; + instanceHandleToTargetMap.set(key, target); +} + /** * Registers the given resize observer and returns a unique ID for it, which * is required to start observing targets. @@ -62,15 +90,16 @@ export function registerObserver( * targets. */ export function unregisterObserver(resizeObserverId: ResizeObserverId): void { - // TODO(ResizeObserver): implement. Once there are no observers left, this - // should disconnect the native module (mirroring - // `IntersectionObserverManager.unregisterObserver`). - registeredResizeObservers.delete(resizeObserverId); + const deleted = registeredResizeObservers.delete(resizeObserverId); + if (deleted && registeredResizeObservers.size === 0) { + NativeResizeObserver?.disconnect(); + isConnected = false; + } } /** * Starts observing a target on a specific resize observer. - * If this is the first target being observed, this should also set up the + * If this is the first target being observed, this also sets up the * centralized notification callback in native. */ export function observe({ @@ -82,7 +111,47 @@ export function observe({ target: ReactNativeElement, box: ResizeObserverBoxOptions, }): void { - // TODO(ResizeObserver): implement. + if (NativeResizeObserver == null) { + throwIfNoNativeResizeObserver(); + return; + } + + const registeredObserver = registeredResizeObservers.get(resizeObserverId); + if (registeredObserver == null) { + console.error( + `ResizeObserverManager: could not start observing target because ResizeObserver with ID ${resizeObserverId} was not registered.`, + ); + return; + } + + const targetNativeNodeReference = getNativeNodeReference(target); + if (targetNativeNodeReference == null) { + // The target is disconnected. We can't observe it anymore. + return; + } + + const instanceHandle = getInstanceHandle(target); + if (instanceHandle == null) { + console.error( + 'ResizeObserverManager: could not find reference to instance handle from target', + ); + return; + } + + // Store the mapping between the instance handle and the target so we can + // access it even after the instance handle has been unmounted. + setTargetForInstanceHandle(instanceHandle, target); + + if (!isConnected) { + NativeResizeObserver.connect(notifyResizeObservers); + isConnected = true; + } + + NativeResizeObserver.observe({ + resizeObserverId, + targetShadowNode: targetNativeNodeReference, + box, + }); } /** @@ -93,13 +162,85 @@ export function unobserve( resizeObserverId: ResizeObserverId, target: ReactNativeElement, ): void { - // TODO(ResizeObserver): implement. + if (NativeResizeObserver == null) { + throwIfNoNativeResizeObserver(); + return; + } + + const registeredObserver = registeredResizeObservers.get(resizeObserverId); + if (registeredObserver == null) { + console.error( + `ResizeObserverManager: could not stop observing target because ResizeObserver with ID ${resizeObserverId} was not registered.`, + ); + return; + } + + const targetNativeNodeReference = getNativeNodeReference(target); + if (targetNativeNodeReference == null) { + // The target is already disconnected, so native doesn't have anything + // to unobserve. + return; + } + + NativeResizeObserver.unobserve(resizeObserverId, targetNativeNodeReference); +} + +/** + * This function is called from native when there are `ResizeObserver` + * entries to dispatch. + */ +function notifyResizeObservers(): void { + trace('ResizeObserverManager.notifyResizeObservers', doNotifyResizeObservers); +} + +function doNotifyResizeObservers(): void { + if (NativeResizeObserver == null) { + throwIfNoNativeResizeObserver(); + return; + } + + const nativeEntries = NativeResizeObserver.takeRecords(); + + const entriesByObserver: Map< + ResizeObserverId, + Array, + > = new Map(); + + for (const nativeEntry of nativeEntries) { + let list = entriesByObserver.get(nativeEntry.resizeObserverId); + if (list == null) { + list = []; + entriesByObserver.set(nativeEntry.resizeObserverId, list); + } + + const target = getTargetFromInstanceHandle( + nativeEntry.targetInstanceHandle, + ); + if (target == null) { + console.warn('Could not find target to create ResizeObserverEntry'); + continue; + } + + list.push(createResizeObserverEntry(nativeEntry, target)); + } + + for (const [resizeObserverId, entriesForObserver] of entriesByObserver) { + const registeredObserver = registeredResizeObservers.get(resizeObserverId); + if (!registeredObserver) { + // This could happen if the observer is disconnected between commit + // and mount. In this case, we can just ignore the entries. + continue; + } + + const {observer, callback} = registeredObserver; + try { + callback.call(observer, entriesForObserver, observer); + } catch (error) { + console.error(error); + } + } } -// TODO(ResizeObserver): implement a `notifyResizeObservers` function that -// will be called from native when there are `ResizeObserver` entries to -// dispatch. It should read the pending entries from -// `NativeResizeObserver.takeRecords()`, group them by observer, and invoke -// each observer's callback (mirroring -// `IntersectionObserverManager.notifyIntersectionObservers` / -// `doNotifyIntersectionObservers`). +function throwIfNoNativeResizeObserver() { + throw new Error('Missing native implementation of ResizeObserver'); +} From 037ec5a9b77890a552cc232a2e9d051704fc5e6b Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Tue, 14 Jul 2026 16:52:56 +0200 Subject: [PATCH 11/39] feat: devicePixelContentBoxSize support --- .../resizeobserver/NativeResizeObserver.cpp | 17 +++++++++++------ .../resizeobserver/NativeResizeObserver.h | 2 ++ .../observers/resize/ResizeObserver.cpp | 9 ++++++++- .../observers/resize/ResizeObserver.h | 7 ++++++- .../webapis/resizeobserver/ResizeObserver.js | 14 +++++++++----- .../resizeobserver/ResizeObserverEntry.js | 19 +++++++++++++++++-- .../specs/NativeResizeObserver.js | 1 + 7 files changed, 54 insertions(+), 15 deletions(-) diff --git a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp index b9e7ef269ca3..47bcf5fbedfc 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp @@ -63,8 +63,6 @@ void NativeResizeObserver::disconnect(jsi::Runtime& runtime) { std::vector NativeResizeObserver::takeRecords( jsi::Runtime& runtime) { - // TODO(ResizeObserver): this will always return an empty vector until - // ResizeObserver::updateStateIfNeeded is implemented. auto entries = resizeObserverManager_.takeRecords(); std::vector nativeModuleEntries; @@ -90,14 +88,16 @@ NativeResizeObserverEntry NativeResizeObserver::convertToNativeModuleEntry( entry.borderBoxSize.width, entry.borderBoxSize.height}; SizeAsTuple contentBoxSize = { entry.contentBoxSize.width, entry.contentBoxSize.height}; - + SizeAsTuple devicePixelContentBoxSize = { + entry.devicePixelContentBoxSize.width, + entry.devicePixelContentBoxSize.height}; NativeResizeObserverEntry nativeModuleEntry = { entry.resizeObserverId, (*entry.shadowNodeFamily).getInstanceHandle(runtime), contentRect, borderBoxSize, contentBoxSize, - }; + devicePixelContentBoxSize}; return nativeModuleEntry; } @@ -109,8 +109,13 @@ UIManager& NativeResizeObserver::getUIManagerFromRuntime( ResizeObserverBoxOptions NativeResizeObserver::boxOptionsFromOptionalString( const std::optional& box) { - if (box.has_value() && box.value() == "border-box") { - return ResizeObserverBoxOptions::BorderBox; + if (box.has_value()) { + if (box.value() == "border-box") { + return ResizeObserverBoxOptions::BorderBox; + } + if (box.value() == "device-pixel-content-box") { + return ResizeObserverBoxOptions::DevicePixelContentBox; + } } // "content-box" is the default value. diff --git a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.h b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.h index 10c20b600020..1b5d293631dd 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.h +++ b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.h @@ -49,6 +49,8 @@ using NativeResizeObserverEntry = NativeResizeObserverNativeResizeObserverEntry< // borderBoxSize SizeAsTuple, // contentBoxSize + SizeAsTuple, + // devicePixelContentBoxSize SizeAsTuple>; template <> diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp index fe60c1594c6d..ff1687e5b0d9 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp @@ -64,8 +64,14 @@ std::optional ResizeObserver::updateStateIfNeeded( layoutMetrics.borderWidth.top}, .size = contentBoxSize}; + auto devicePixelContentBoxSize = Size{ + contentBoxSize.width * layoutMetrics.pointScaleFactor, + contentBoxSize.height * layoutMetrics.pointScaleFactor}; + auto observedSize = boxOptions_ == ResizeObserverBoxOptions::ContentBox ? contentBoxSize + : boxOptions_ == ResizeObserverBoxOptions::DevicePixelContentBox + ? devicePixelContentBoxSize : borderBoxSize; if (lastReportedSize_.has_value() && @@ -80,7 +86,8 @@ std::optional ResizeObserver::updateStateIfNeeded( targetShadowNodeFamily_, contentRect, borderBoxSize, - contentBoxSize}; + contentBoxSize, + devicePixelContentBoxSize}; } } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h index 795067dada0f..5d7d65f15652 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h @@ -20,7 +20,11 @@ using ResizeObserverObserverId = int32_t; // Corresponds to the `box` option of `ResizeObserver#observe`. // https://w3c.github.io/csswg-drafts/resize-observer/#resize-observer-box-options -enum class ResizeObserverBoxOptions { ContentBox, BorderBox }; +enum class ResizeObserverBoxOptions { + ContentBox, + BorderBox, + DevicePixelContentBox +}; struct ResizeObserverEntry { ResizeObserverObserverId resizeObserverId; @@ -28,6 +32,7 @@ struct ResizeObserverEntry { Rect contentRect; Size borderBoxSize; Size contentBoxSize; + Size devicePixelContentBoxSize; bool sameShadowNodeFamily( const ShadowNodeFamily& otherShadowNodeFamily) const { diff --git a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js index d62b24bdeaf0..303edf9a4992 100644 --- a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js +++ b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js @@ -26,7 +26,10 @@ export type ResizeObserverCallback = ( * Corresponds to the `box` option of `ResizeObserver#observe`. * https://drafts.csswg.org/resize-observer/#resize-observer-box-options */ -export type ResizeObserverBoxOptions = 'content-box' | 'border-box'; +export type ResizeObserverBoxOptions = + | 'content-box' + | 'border-box' + | 'device-pixel-content-box'; export interface ResizeObserverOptions { box?: ResizeObserverBoxOptions; @@ -44,9 +47,6 @@ export interface ResizeObserverOptions { * * You can add/remove multiple target elements to/from a single * `ResizeObserver` instance. - * - * TODO(ResizeObserver): this is a stub implementation. `observe`, - * `unobserve` and `disconnect` do not yet notify any callback. */ export default class ResizeObserver { _callback: ResizeObserverCallback; @@ -176,7 +176,11 @@ function normalizeBoxOption( return 'content-box'; } - if (box !== 'content-box' && box !== 'border-box') { + if ( + box !== 'content-box' && + box !== 'border-box' && + box !== 'device-pixel-content-box' + ) { throw new TypeError( `Failed to execute 'observe' on 'ResizeObserver': Failed to read the 'box' property from 'ResizeObserverOptions': The provided value '${String( box, diff --git a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverEntry.js b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverEntry.js index 7d067b94ea19..f1dbb41ddb1d 100644 --- a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverEntry.js +++ b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverEntry.js @@ -22,8 +22,6 @@ import ResizeObserverSize from './ResizeObserverSize'; * interface of the Resize Observer API represents the object passed to the * `ResizeObserver()` callback function, which allows access to the new * dimensions of the observed target element after its size has changed. - * - * Note: `devicePixelContentBoxSize` is not supported in React Native. */ export default class ResizeObserverEntry { // We lazily compute all the properties from the raw entry provided by the @@ -87,6 +85,23 @@ export default class ResizeObserverEntry { const contentBoxSize = this._nativeEntry.contentBoxSize; return [new ResizeObserverSize(contentBoxSize[0], contentBoxSize[1])]; } + + /** + * An array containing the new device pixel content box size of the observed element. + * + * Note: React Native does not support fragments, so this array will + * always contain a single value. + */ + get devicePixelContentBoxSize(): ReadonlyArray { + const devicePixelContentBoxSize = + this._nativeEntry.devicePixelContentBoxSize; + return [ + new ResizeObserverSize( + devicePixelContentBoxSize[0], + devicePixelContentBoxSize[1], + ), + ]; + } } setPlatformObject(ResizeObserverEntry); diff --git a/packages/react-native/src/private/webapis/resizeobserver/specs/NativeResizeObserver.js b/packages/react-native/src/private/webapis/resizeobserver/specs/NativeResizeObserver.js index 33640952c8a2..3175f942cc5a 100644 --- a/packages/react-native/src/private/webapis/resizeobserver/specs/NativeResizeObserver.js +++ b/packages/react-native/src/private/webapis/resizeobserver/specs/NativeResizeObserver.js @@ -18,6 +18,7 @@ export type NativeResizeObserverEntry = { contentRect: ReadonlyArray, // It's actually a tuple with x, y, width and height borderBoxSize: ReadonlyArray, // It's actually a tuple with inlineSize and blockSize contentBoxSize: ReadonlyArray, // It's actually a tuple with inlineSize and blockSize + devicePixelContentBoxSize: ReadonlyArray, // It's actually a tuple with inlineSize and blockSize }; export type NativeResizeObserverObserveOptions = { From 3cbec1487ba35aa100127c3805541e3026383aa5 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Tue, 14 Jul 2026 17:23:20 +0200 Subject: [PATCH 12/39] feat: broadcast on rendering --- .../resizeobserver/CMakeLists.txt | 1 + .../resizeobserver/NativeResizeObserver.cpp | 9 +- .../React-resizeobservernativemodule.podspec | 1 + .../renderer/observers/resize/CMakeLists.txt | 1 + .../resize/ResizeObserverManager.cpp | 176 ++++++++++++------ .../observers/resize/ResizeObserverManager.h | 36 +++- .../runtimescheduler/RuntimeScheduler.cpp | 6 + .../runtimescheduler/RuntimeScheduler.h | 6 + .../RuntimeSchedulerResizeObserverDelegate.h | 24 +++ .../RuntimeScheduler_Legacy.cpp | 5 + .../RuntimeScheduler_Legacy.h | 3 + .../RuntimeScheduler_Modern.cpp | 13 ++ .../RuntimeScheduler_Modern.h | 4 + 13 files changed, 221 insertions(+), 64 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeSchedulerResizeObserverDelegate.h diff --git a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/CMakeLists.txt b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/CMakeLists.txt index 2f22a7fd5393..c3e707910408 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/CMakeLists.txt @@ -20,6 +20,7 @@ target_link_libraries(react_nativemodule_resizeobserver react_renderer_core react_renderer_graphics react_renderer_observers_resize + react_renderer_runtimescheduler react_renderer_uimanager rrc_view ) diff --git a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp index 47bcf5fbedfc..8d4bf2f09fa6 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp @@ -7,6 +7,7 @@ #include "NativeResizeObserver.h" #include +#include #include #include @@ -53,12 +54,16 @@ void NativeResizeObserver::connect( AsyncCallback<> notifyResizeObserversFunction) { auto& uiManager = getUIManagerFromRuntime(runtime); resizeObserverManager_.connect( - uiManager, std::move(notifyResizeObserversFunction)); + *RuntimeSchedulerBinding::getBinding(runtime)->getRuntimeScheduler(), + uiManager, + std::move(notifyResizeObserversFunction)); } void NativeResizeObserver::disconnect(jsi::Runtime& runtime) { auto& uiManager = getUIManagerFromRuntime(runtime); - resizeObserverManager_.disconnect(uiManager); + resizeObserverManager_.disconnect( + *RuntimeSchedulerBinding::getBinding(runtime)->getRuntimeScheduler(), + uiManager); } std::vector NativeResizeObserver::takeRecords( diff --git a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/React-resizeobservernativemodule.podspec b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/React-resizeobservernativemodule.podspec index db9fb8b6ecc3..2d82927ded17 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/React-resizeobservernativemodule.podspec +++ b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/React-resizeobservernativemodule.podspec @@ -59,6 +59,7 @@ Pod::Spec.new do |s| s.dependency "React-Fabric" s.dependency "React-Fabric/bridging" s.dependency "React-Fabric/observers/resize" + s.dependency "React-runtimescheduler" add_dependency(s, "React-RCTFBReactNativeSpec") add_dependency(s, "React-runtimeexecutor", :additional_framework_paths => ["platform/ios"]) add_dependency(s, "React-graphics", :additional_framework_paths => ["react/renderer/graphics/platform/ios"]) diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/observers/resize/CMakeLists.txt index a38b110e7604..84949939fecd 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/CMakeLists.txt @@ -19,6 +19,7 @@ target_link_libraries(react_renderer_observers_resize react_renderer_core react_renderer_graphics react_renderer_mounting + react_renderer_runtimescheduler react_renderer_uimanager ) target_compile_reactnative_options(react_renderer_observers_resize PRIVATE) diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp index 44a36951eb25..6d6a6aa3d569 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp @@ -35,42 +35,24 @@ void ResizeObserverManager::observe( ResizeObserverObserverId resizeObserverId, const ShadowNodeFamily::Shared& shadowNodeFamily, ResizeObserverBoxOptions boxOptions, - UIManager& uiManager) { + UIManager& /*uiManager*/) { TraceSection s("ResizeObserverManager::observe"); auto surfaceId = shadowNodeFamily->getSurfaceId(); - // Per spec, `observe` must perform an initial observation synchronously, - // using the shadow tree that is current at the time `observe` is called - // (i.e., independent of the commit hook, which only runs for subsequent - // commits). - RootShadowNode::Shared rootShadowNode = - getRootShadowNode(surfaceId, uiManager.getShadowTreeRegistry()); - - std::optional entry; - - { - std::unique_lock lock(observersMutex_); - - auto& observers = observersBySurfaceId_[surfaceId]; - - observers.emplace_back(std::make_unique( - resizeObserverId, shadowNodeFamily, boxOptions)); - - auto* observer = observers.back().get(); + // Per spec, newly observed targets are only processed on the next "update + // the rendering" step (i.e. the `runResizeObservations` event-loop step + // below), not synchronously here. Since `observe` always runs as part of a + // JS task, and every task is followed by an "update the rendering" step + // regardless of whether there are pending rendering (mounting) updates, + // the initial observation is still guaranteed to be delivered promptly, at + // the end of the current tick. + std::unique_lock lock(observersMutex_); - if (rootShadowNode != nullptr) { - entry = observer->updateStateIfNeeded(*rootShadowNode); - } - } + auto& observers = observersBySurfaceId_[surfaceId]; - if (entry) { - { - std::unique_lock lock(pendingEntriesMutex_); - pendingEntries_.push_back(std::move(entry).value()); - } - notifyObserversIfNecessary(); - } + observers.emplace_back(std::make_unique( + resizeObserverId, shadowNodeFamily, boxOptions)); } void ResizeObserverManager::unobserve( @@ -105,6 +87,7 @@ void ResizeObserverManager::unobserve( } void ResizeObserverManager::connect( + RuntimeScheduler& runtimeScheduler, UIManager& uiManager, std::function notifyResizeObserversFunction) { TraceSection s("ResizeObserverManager::connect"); @@ -116,11 +99,15 @@ void ResizeObserverManager::connect( notifyResizeObserversFunction_ = std::move(notifyResizeObserversFunction); + runtimeScheduler.setResizeObserverDelegate(this); uiManager.registerCommitHook(*this); + shadowTreeRegistry_ = &uiManager.getShadowTreeRegistry(); commitHookRegistered_ = true; } -void ResizeObserverManager::disconnect(UIManager& uiManager) { +void ResizeObserverManager::disconnect( + RuntimeScheduler& runtimeScheduler, + UIManager& uiManager) { TraceSection s("ResizeObserverManager::disconnect"); // Fail-safe in case the caller doesn't guarantee consistency. @@ -128,7 +115,9 @@ void ResizeObserverManager::disconnect(UIManager& uiManager) { return; } + runtimeScheduler.setResizeObserverDelegate(nullptr); uiManager.unregisterCommitHook(*this); + shadowTreeRegistry_ = nullptr; notifyResizeObserversFunction_ = nullptr; commitHookRegistered_ = false; @@ -153,59 +142,134 @@ void ResizeObserverManager::commitHookWasUnregistered( void ResizeObserverManager::shadowTreeDidCommit( const ShadowTree& shadowTree, - const RootShadowNode::Shared& rootShadowNode, + const RootShadowNode::Shared& /*rootShadowNode*/, const std::vector& affectedLayoutableNodes) noexcept { - runResizeObservations(shadowTree, *rootShadowNode, affectedLayoutableNodes); + TraceSection s("ResizeObserverManager::shadowTreeDidCommit"); + + // This runs on the commit hook, which may execute on any thread. It must + // not compute observations or notify JS: it only collects which observed + // targets went dirty, so that `runResizeObservations` (invoked from the + // event-loop step, always on the JS thread) can compute their new sizes + // exactly once against the latest committed tree. + auto surfaceId = shadowTree.getSurfaceId(); + + std::unordered_set observedFamilies; + { + std::unique_lock lock(observersMutex_); + + auto observersIt = observersBySurfaceId_.find(surfaceId); + if (observersIt == observersBySurfaceId_.end()) { + // No observers registered for this surface: nothing to collect. + return; + } + + observedFamilies.reserve(observersIt->second.size()); + for (auto& observer : observersIt->second) { + observedFamilies.insert(observer->getTargetShadowNodeFamily().get()); + } + } + + std::unordered_set newlyDirtyFamilies; + for (const auto* node : affectedLayoutableNodes) { + const auto* family = &node->getFamily(); + if (observedFamilies.contains(family)) { + newlyDirtyFamilies.insert(family); + } + } + + if (newlyDirtyFamilies.empty()) { + return; + } + + std::unique_lock lock(dirtyFamiliesMutex_); + auto& dirtyFamilies = dirtyFamiliesBySurfaceId_[surfaceId]; + dirtyFamilies.insert(newlyDirtyFamilies.begin(), newlyDirtyFamilies.end()); } -#pragma mark - Private methods +#pragma mark - RuntimeSchedulerResizeObserverDelegate void ResizeObserverManager::runResizeObservations( - const ShadowTree& shadowTree, - const RootShadowNode& rootShadowNode, - const std::vector& affectedLayoutableNodes) { + const std::unordered_set< + SurfaceId>& /*surfaceIdsWithPendingRenderingUpdates*/) { TraceSection s("ResizeObserverManager::runResizeObservations"); - auto surfaceId = shadowTree.getSurfaceId(); + // Drain the dirty families collected by the commit hook since the last + // time this step ran. + std::unordered_map> + dirtyFamiliesBySurfaceId; + { + std::unique_lock lock(dirtyFamiliesMutex_); + dirtyFamiliesBySurfaceId.swap(dirtyFamiliesBySurfaceId_); + } - // Build a set of the families whose layout actually changed in this - // commit, so we only recompute observers that could possibly have a new - // size. - std::unordered_set affectedFamilies; - affectedFamilies.reserve(affectedLayoutableNodes.size()); - for (const auto* node : affectedLayoutableNodes) { - affectedFamilies.insert(&node->getFamily()); + // A surface is relevant to this step if either (a) it has dirty families + // collected from a commit, or (b) it has observers that have never + // reported yet (e.g. observers just registered via `observe`, which no + // longer perform a synchronous initial observation). + std::unordered_set candidateSurfaceIds; + for (const auto& [surfaceId, families] : dirtyFamiliesBySurfaceId) { + candidateSurfaceIds.insert(surfaceId); + } + + { + std::unique_lock lock(observersMutex_); + for (const auto& [surfaceId, observers] : observersBySurfaceId_) { + for (const auto& observer : observers) { + if (!observer->hasReported()) { + candidateSurfaceIds.insert(surfaceId); + break; + } + } + } + } + + if (candidateSurfaceIds.empty()) { + return; } std::vector entries; - { + for (auto surfaceId : candidateSurfaceIds) { + RootShadowNode::Shared rootShadowNode = + getRootShadowNode(surfaceId, *shadowTreeRegistry_); + if (rootShadowNode == nullptr) { + continue; + } + + auto dirtyFamiliesIt = dirtyFamiliesBySurfaceId.find(surfaceId); + const std::unordered_set* dirtyFamilies = + dirtyFamiliesIt != dirtyFamiliesBySurfaceId.end() + ? &dirtyFamiliesIt->second + : nullptr; + std::unique_lock lock(observersMutex_); auto observersIt = observersBySurfaceId_.find(surfaceId); if (observersIt == observersBySurfaceId_.end()) { - return; + continue; } - auto& observers = observersIt->second; - for (auto& observer : observers) { - // Only recompute observers whose target was affected by this commit, - // or that have never reported yet (e.g. an initial observation that - // raced with a commit before it could run). - bool wasAffected = affectedFamilies.contains( - observer->getTargetShadowNodeFamily().get()); + for (auto& observer : observersIt->second) { + // Only recompute observers whose target was affected by a commit + // since the last pass, or that have never reported yet. + bool wasAffected = dirtyFamilies != nullptr && + dirtyFamilies->contains(observer->getTargetShadowNodeFamily().get()); if (!wasAffected && observer->hasReported()) { continue; } - auto entry = observer->updateStateIfNeeded(rootShadowNode); + auto entry = observer->updateStateIfNeeded(*rootShadowNode); if (entry) { entries.push_back(std::move(entry).value()); } } } + if (entries.empty()) { + return; + } + { std::unique_lock lock(pendingEntriesMutex_); pendingEntries_.insert( @@ -215,6 +279,8 @@ void ResizeObserverManager::runResizeObservations( notifyObserversIfNecessary(); } +#pragma mark - Private methods + void ResizeObserverManager::notifyObserversIfNecessary() { bool dispatchNotification = false; diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h index e75eb2f6909f..c0e2e17a090e 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h @@ -9,18 +9,24 @@ #include #include +#include +#include +#include #include #include #include #include #include #include +#include #include #include "ResizeObserver.h" namespace facebook::react { -class ResizeObserverManager final : public UIManagerCommitHook { +class ResizeObserverManager final + : public UIManagerCommitHook, + public RuntimeSchedulerResizeObserverDelegate { public: ResizeObserverManager(); @@ -35,13 +41,20 @@ class ResizeObserverManager final : public UIManagerCommitHook { const ShadowNodeFamily::Shared& shadowNodeFamily); void connect( + RuntimeScheduler& runtimeScheduler, UIManager& uiManager, std::function notifyResizeObserversFunction); - void disconnect(UIManager& uiManager); + void disconnect(RuntimeScheduler& runtimeScheduler, UIManager& uiManager); std::vector takeRecords(); +#pragma mark - RuntimeSchedulerResizeObserverDelegate + + void runResizeObservations( + const std::unordered_set& + surfaceIdsWithPendingRenderingUpdates) override; + #pragma mark - UIManagerCommitHook void commitHookWasRegistered(const UIManager& uiManager) noexcept override; @@ -59,18 +72,27 @@ class ResizeObserverManager final : public UIManagerCommitHook { observersBySurfaceId_; mutable std::mutex observersMutex_; + // Families that went dirty (i.e. were part of `affectedLayoutableNodes`) + // since the last time `runResizeObservations` drained this map. Populated + // by the commit hook (potentially off the JS thread) and drained by the + // event-loop step (always on the JS thread). + std::unordered_map> + dirtyFamiliesBySurfaceId_; + std::mutex dirtyFamiliesMutex_; + std::function notifyResizeObserversFunction_; bool commitHookRegistered_{}; + // This is only accessed from the JS thread at the end of the event loop + // tick, so it is safe to retain it as a raw pointer. + // We need to retain it here because the RuntimeScheduler does not provide + // it when calling `runResizeObservations`. + const ShadowTreeRegistry* shadowTreeRegistry_{nullptr}; + mutable std::vector pendingEntries_; mutable std::mutex pendingEntriesMutex_; mutable bool notifiedResizeObservers_{}; - void runResizeObservations( - const ShadowTree& shadowTree, - const RootShadowNode& rootShadowNode, - const std::vector& affectedLayoutableNodes); - void notifyObserversIfNecessary(); void notifyObservers(); }; diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp index c9df8025a1fb..3c560e9529e9 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp @@ -153,4 +153,10 @@ void RuntimeScheduler::setIntersectionObserverDelegate( intersectionObserverDelegate); } +void RuntimeScheduler::setResizeObserverDelegate( + RuntimeSchedulerResizeObserverDelegate* resizeObserverDelegate) { + return runtimeSchedulerImpl_->setResizeObserverDelegate( + resizeObserverDelegate); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h index 922e1c7701a2..ecd45d075144 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h @@ -16,6 +16,7 @@ #include #include "RuntimeSchedulerEventTimingDelegate.h" #include "RuntimeSchedulerIntersectionObserverDelegate.h" +#include "RuntimeSchedulerResizeObserverDelegate.h" namespace facebook::react { @@ -55,6 +56,8 @@ class RuntimeSchedulerBase : public facebook::hermes::IEventLoopControl { virtual void setEventTimingDelegate(RuntimeSchedulerEventTimingDelegate *eventTimingDelegate) = 0; virtual void setIntersectionObserverDelegate( RuntimeSchedulerIntersectionObserverDelegate *intersectionObserverDelegate) = 0; + virtual void setResizeObserverDelegate( + RuntimeSchedulerResizeObserverDelegate* resizeObserverDelegate) = 0; }; // This is a proxy for RuntimeScheduler implementation, which will be selected @@ -167,6 +170,9 @@ class RuntimeScheduler final : public RuntimeSchedulerBase { void setIntersectionObserverDelegate( RuntimeSchedulerIntersectionObserverDelegate *intersectionObserverDelegate) override; + void setResizeObserverDelegate( + RuntimeSchedulerResizeObserverDelegate* resizeObserverDelegate) override; + private: // Actual implementation, stored as a unique pointer to simplify memory // management. diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeSchedulerResizeObserverDelegate.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeSchedulerResizeObserverDelegate.h new file mode 100644 index 000000000000..d5aaf5c5c514 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeSchedulerResizeObserverDelegate.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { + +using SurfaceId = int32_t; + +class RuntimeSchedulerResizeObserverDelegate { + public: + virtual ~RuntimeSchedulerResizeObserverDelegate() = default; + + virtual void runResizeObservations( + const std::unordered_set &surfaceIdsWithPendingRenderingUpdates) = 0; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp index d55b3eab268f..90057c69124b 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp @@ -222,6 +222,11 @@ void RuntimeScheduler_Legacy::setIntersectionObserverDelegate( // No-op in the legacy scheduler } +void RuntimeScheduler_Legacy::setResizeObserverDelegate( + RuntimeSchedulerResizeObserverDelegate* /*resizeObserverDelegate*/) { + // No-op in the legacy scheduler +} + #pragma mark - Private void RuntimeScheduler_Legacy::scheduleWorkLoopIfNecessary() { diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h index 7306df99b973..7ccb35ceb583 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h @@ -133,6 +133,9 @@ class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase { void setIntersectionObserverDelegate( RuntimeSchedulerIntersectionObserverDelegate *intersectionObserverDelegate) override; + void setResizeObserverDelegate( + RuntimeSchedulerResizeObserverDelegate* resizeObserverDelegate) override; + private: std::priority_queue, std::vector>, TaskPriorityComparer> taskQueue_; diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp index 5a7e69db108f..f5dc4eb72aa4 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp @@ -232,6 +232,11 @@ void RuntimeScheduler_Modern::setIntersectionObserverDelegate( intersectionObserverDelegate_ = intersectionObserverDelegate; } +void RuntimeScheduler_Modern::setResizeObserverDelegate( + RuntimeSchedulerResizeObserverDelegate* resizeObserverDelegate) { + resizeObserverDelegate_ = resizeObserverDelegate; +} + #pragma mark - Private void RuntimeScheduler_Modern::scheduleTask(std::shared_ptr task) { @@ -357,6 +362,14 @@ void RuntimeScheduler_Modern::updateRendering(HighResTimeStamp taskEndTime) { taskEndTime, surfaceIdsWithPendingRenderingUpdates_); } + // This is the integration of the Resize Observer API in the Event Loop. + // See + // https://w3c.github.io/csswg-drafts/resize-observer/#broadcast-resize-notifications-h + if (resizeObserverDelegate_ != nullptr) { + resizeObserverDelegate_->runResizeObservations( + surfaceIdsWithPendingRenderingUpdates_); + } + // This is the integration of the Intersection Observer API in the Event Loop. // See if (intersectionObserverDelegate_ != nullptr) { diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h index 624676b22755..02ba731fce5b 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h @@ -151,6 +151,9 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { void setIntersectionObserverDelegate( RuntimeSchedulerIntersectionObserverDelegate *intersectionObserverDelegate) override; + void setResizeObserverDelegate( + RuntimeSchedulerResizeObserverDelegate* resizeObserverDelegate) override; + private: /// Monotonic counter handing out IDs for IEventLoopControl task queue /// sources. @@ -225,6 +228,7 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { PerformanceEntryReporter *performanceEntryReporter_{nullptr}; RuntimeSchedulerIntersectionObserverDelegate *intersectionObserverDelegate_{nullptr}; + RuntimeSchedulerResizeObserverDelegate* resizeObserverDelegate_{nullptr}; RuntimeSchedulerTaskErrorHandler onTaskError_; }; From 38b01b489f81e3138bdf6ce80720a422680b2652 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 15 Jul 2026 16:16:34 +0200 Subject: [PATCH 13/39] fix: handle re-observe --- .../webapis/resizeobserver/ResizeObserver.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js index 303edf9a4992..6aafc4bde4a8 100644 --- a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js +++ b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js @@ -93,8 +93,20 @@ export default class ResizeObserver { const box = normalizeBoxOption(options?.box); if (this._observationTargets.has(target)) { - // Re-observing an already observed target just updates its `box` - // option (per spec, this is equivalent to calling `unobserve` first). + // Per spec, re-observing replaces the previous observation (equivalent + // to unobserve + observe) and must update native state. + const previousBox = this._observationTargets.get(target); + if (previousBox === box) { + return; + } + + const resizeObserverId = this._getOrCreateResizeObserverId(); + ResizeObserverManager.unobserve(resizeObserverId, target); + ResizeObserverManager.observe({ + resizeObserverId, + target, + box, + }); this._observationTargets.set(target, box); return; } From 225b60c4e8e8982164ec548411b5b8281dacf307 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 15 Jul 2026 16:17:25 +0200 Subject: [PATCH 14/39] fix: initial load + hidden cases --- .../observers/resize/ResizeObserver.cpp | 134 +++++++++++++++--- .../observers/resize/ResizeObserver.h | 14 +- .../resize/ResizeObserverManager.cpp | 11 +- 3 files changed, 126 insertions(+), 33 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp index ff1687e5b0d9..3d35b90d3319 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp @@ -7,11 +7,76 @@ #include "ResizeObserver.h" #include +#include +#include #include #include namespace facebook::react { +namespace { + +const ShadowNode* getTargetShadowNode( + const ShadowNodeFamily::AncestorList& ancestors) { + if (ancestors.empty()) { + return nullptr; + } + + const auto& parentChildPair = ancestors.back(); + return parentChildPair.first.get() + .getChildren() + .at(parentChildPair.second) + .get(); +} + +bool isTargetHidden(const ShadowNode& targetShadowNode) { + // `display: 'none'` sets the Hidden trait on View nodes when props are + // committed, which can be ahead of the last laid-out `displayType` / frame. + if (targetShadowNode.getTraits().check(ShadowNodeTraits::Trait::Hidden)) { + return true; + } + + if (const auto* layoutableShadowNode = + dynamic_cast(&targetShadowNode)) { + return layoutableShadowNode->getLayoutMetrics().displayType == + DisplayType::None; + } + + return false; +} + +Size getObservedSize( + ResizeObserverBoxOptions boxOptions, + Size borderBoxSize, + Size contentBoxSize, + Size devicePixelContentBoxSize) { + if (boxOptions == ResizeObserverBoxOptions::ContentBox) { + return contentBoxSize; + } + if (boxOptions == ResizeObserverBoxOptions::DevicePixelContentBox) { + return devicePixelContentBoxSize; + } + return borderBoxSize; +} + +ResizeObserverEntry makeResizeObserverEntry( + ResizeObserverObserverId resizeObserverId, + const ShadowNodeFamily::Shared& targetShadowNodeFamily, + Size borderBoxSize, + Size contentBoxSize, + Size devicePixelContentBoxSize, + Rect contentRect) { + return ResizeObserverEntry{ + resizeObserverId, + targetShadowNodeFamily, + contentRect, + borderBoxSize, + contentBoxSize, + devicePixelContentBoxSize}; +} + +} // namespace + ResizeObserver::ResizeObserver( ResizeObserverObserverId resizeObserverId, ShadowNodeFamily::Shared targetShadowNodeFamily, @@ -26,17 +91,50 @@ std::optional ResizeObserver::updateStateIfNeeded( const RootShadowNode& rootShadowNode) { auto ancestors = targetShadowNodeFamily_->getAncestors(rootShadowNode); - auto layoutMetrics = ancestors.empty() - ? EmptyLayoutMetrics - : LayoutableShadowNode::computeRelativeLayoutMetrics( - ancestors, - {.includeTransform = false, .includeViewportOffset = true}); + if (ancestors.empty()) { + // The target is not part of the tree. If it is later reinserted, treat it + // as a brand new observation. + lastReportedSize_.reset(); + return std::nullopt; + } + + const auto* targetShadowNode = getTargetShadowNode(ancestors); + if (targetShadowNode == nullptr) { + lastReportedSize_.reset(); + return std::nullopt; + } + + const bool isInitialDelivery = !lastReportedSize_.has_value(); + + // Per spec (and browser behavior), `display: none` triggers a notification + // with zero-sized boxes. Use the Hidden trait so we do not rely on possibly + // stale layout metrics from before the hide commit was laid out. + if (isTargetHidden(*targetShadowNode)) { + Size zeroSize{0, 0}; + Rect zeroContentRect{.origin = {0, 0}, .size = zeroSize}; + auto observedSize = getObservedSize( + boxOptions_, zeroSize, zeroSize, zeroSize); + + if (!isInitialDelivery && lastReportedSize_.value() == observedSize) { + return std::nullopt; + } + + lastReportedSize_ = observedSize; + + return makeResizeObserverEntry( + resizeObserverId_, + targetShadowNodeFamily_, + zeroSize, + zeroSize, + zeroSize, + zeroContentRect); + } + + auto layoutMetrics = LayoutableShadowNode::computeRelativeLayoutMetrics( + ancestors, + {.includeTransform = false, .includeViewportOffset = true}); if (layoutMetrics == EmptyLayoutMetrics) { - // The target is not (or no longer) part of the tree rooted at - // `rootShadowNode`. Per spec, if the element is later reinserted into the - // tree it must be treated as a brand new observation, so we reset the - // last reported size. lastReportedSize_.reset(); return std::nullopt; } @@ -68,26 +166,24 @@ std::optional ResizeObserver::updateStateIfNeeded( contentBoxSize.width * layoutMetrics.pointScaleFactor, contentBoxSize.height * layoutMetrics.pointScaleFactor}; - auto observedSize = boxOptions_ == ResizeObserverBoxOptions::ContentBox - ? contentBoxSize - : boxOptions_ == ResizeObserverBoxOptions::DevicePixelContentBox - ? devicePixelContentBoxSize - : borderBoxSize; + auto observedSize = getObservedSize( + boxOptions_, borderBoxSize, contentBoxSize, devicePixelContentBoxSize); - if (lastReportedSize_.has_value() && - lastReportedSize_.value() == observedSize) { + // Skip when the observed box is unchanged. The first delivery always runs + // (including 0x0 content-box), matching browser behavior on `observe()`. + if (!isInitialDelivery && lastReportedSize_.value() == observedSize) { return std::nullopt; } lastReportedSize_ = observedSize; - return ResizeObserverEntry{ + return makeResizeObserverEntry( resizeObserverId_, targetShadowNodeFamily_, - contentRect, borderBoxSize, contentBoxSize, - devicePixelContentBoxSize}; + devicePixelContentBoxSize, + contentRect); } } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h index 5d7d65f15652..86405045d603 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h @@ -65,11 +65,10 @@ class ResizeObserver { return boxOptions_; } - // Whether this observer has ever produced an entry (i.e. reported a size - // at least once since being created or since the target was last removed - // from the tree). - bool hasReported() const { - return lastReportedSize_.has_value(); + // Whether this observation still needs its first delivery at the next + // `runResizeObservations` step (just registered, or target was removed). + bool needsInitialDeliveryCheck() const { + return !lastReportedSize_.has_value(); } private: @@ -77,9 +76,8 @@ class ResizeObserver { ShadowNodeFamily::Shared targetShadowNodeFamily_; ResizeObserverBoxOptions boxOptions_; - // Tracks the "last reported size" for this observation, per the - // ResizeObserver spec. - // https://w3c.github.io/csswg-drafts/resize-observer/#resize-observer-last-reported-size-slot + // Last delivered observed-box size. Empty until the first entry is produced. + // Reset when the target leaves the tree so a reinsert is a new observation. std::optional lastReportedSize_; }; diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp index 6d6a6aa3d569..e3179a3847e9 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp @@ -204,9 +204,8 @@ void ResizeObserverManager::runResizeObservations( } // A surface is relevant to this step if either (a) it has dirty families - // collected from a commit, or (b) it has observers that have never - // reported yet (e.g. observers just registered via `observe`, which no - // longer perform a synchronous initial observation). + // collected from a commit, or (b) it has observers awaiting their first + // delivery check (e.g. observers just registered via `observe()`). std::unordered_set candidateSurfaceIds; for (const auto& [surfaceId, families] : dirtyFamiliesBySurfaceId) { candidateSurfaceIds.insert(surfaceId); @@ -216,7 +215,7 @@ void ResizeObserverManager::runResizeObservations( std::unique_lock lock(observersMutex_); for (const auto& [surfaceId, observers] : observersBySurfaceId_) { for (const auto& observer : observers) { - if (!observer->hasReported()) { + if (observer->needsInitialDeliveryCheck()) { candidateSurfaceIds.insert(surfaceId); break; } @@ -252,10 +251,10 @@ void ResizeObserverManager::runResizeObservations( for (auto& observer : observersIt->second) { // Only recompute observers whose target was affected by a commit - // since the last pass, or that have never reported yet. + // since the last pass, or that still need their first delivery check. bool wasAffected = dirtyFamilies != nullptr && dirtyFamilies->contains(observer->getTargetShadowNodeFamily().get()); - if (!wasAffected && observer->hasReported()) { + if (!wasAffected && !observer->needsInitialDeliveryCheck()) { continue; } From 502cea38a64dac223ccb0fed5e181e65bd0d5eaa Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Thu, 23 Jul 2026 10:10:47 +0200 Subject: [PATCH 15/39] fix: unobserve after target unmount --- .../resizeobserver/NativeResizeObserver.cpp | 32 ++++++++++++++--- .../resizeobserver/NativeResizeObserver.h | 4 +-- .../webapis/resizeobserver/ResizeObserver.js | 27 ++++++++------ .../internals/ResizeObserverManager.js | 36 +++++++++++++------ .../specs/NativeResizeObserver.js | 8 +++-- 5 files changed, 77 insertions(+), 30 deletions(-) diff --git a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp index 8d4bf2f09fa6..5505aac30898 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.cpp @@ -24,11 +24,32 @@ NativeResizeObserverModuleProvider( namespace facebook::react { +namespace { + +jsi::Object tokenFromShadowNodeFamily( + jsi::Runtime& runtime, + ShadowNodeFamily::Shared shadowNodeFamily) { + jsi::Object obj(runtime); + // Need to const_cast since JSI only allows non-const pointees + obj.setNativeState( + runtime, + std::const_pointer_cast(std::move(shadowNodeFamily))); + return obj; +} + +ShadowNodeFamily::Shared shadowNodeFamilyFromToken( + jsi::Runtime& runtime, + jsi::Object token) { + return token.getNativeState(runtime); +} + +} // namespace + NativeResizeObserver::NativeResizeObserver( std::shared_ptr jsInvoker) : NativeResizeObserverCxxSpec(std::move(jsInvoker)) {} -void NativeResizeObserver::observe( +jsi::Object NativeResizeObserver::observe( jsi::Runtime& runtime, NativeResizeObserverObserveOptions options) { auto resizeObserverId = options.resizeObserverId; @@ -39,13 +60,16 @@ void NativeResizeObserver::observe( resizeObserverManager_.observe( resizeObserverId, shadowNodeFamily, boxOptions, uiManager); + + return tokenFromShadowNodeFamily(runtime, shadowNodeFamily); } void NativeResizeObserver::unobserve( - jsi::Runtime& /*runtime*/, + jsi::Runtime& runtime, NativeResizeObserverResizeObserverId resizeObserverId, - std::shared_ptr targetShadowNode) { - auto shadowNodeFamily = targetShadowNode->getFamilyShared(); + jsi::Object targetToken) { + auto shadowNodeFamily = + shadowNodeFamilyFromToken(runtime, std::move(targetToken)); resizeObserverManager_.unobserve(resizeObserverId, shadowNodeFamily); } diff --git a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.h b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.h index 1b5d293631dd..fcfe5c6b7ba5 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.h +++ b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/NativeResizeObserver.h @@ -63,14 +63,14 @@ class NativeResizeObserver public: NativeResizeObserver(std::shared_ptr jsInvoker); - void observe( + jsi::Object observe( jsi::Runtime& runtime, NativeResizeObserverObserveOptions options); void unobserve( jsi::Runtime& runtime, NativeResizeObserverResizeObserverId resizeObserverId, - std::shared_ptr targetShadowNode); + jsi::Object targetToken); void connect( jsi::Runtime& runtime, diff --git a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js index 6aafc4bde4a8..ace52ab31870 100644 --- a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js +++ b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js @@ -100,24 +100,29 @@ export default class ResizeObserver { return; } - const resizeObserverId = this._getOrCreateResizeObserverId(); - ResizeObserverManager.unobserve(resizeObserverId, target); - ResizeObserverManager.observe({ - resizeObserverId, + ResizeObserverManager.unobserve( + this._getOrCreateResizeObserverId(), target, - box, - }); - this._observationTargets.set(target, box); - return; + ); + this._observationTargets.delete(target); } - ResizeObserverManager.observe({ - resizeObserverId: this._getOrCreateResizeObserverId(), + const resizeObserverId = this._getOrCreateResizeObserverId(); + const didObserve = ResizeObserverManager.observe({ + resizeObserverId, target, box, }); - this._observationTargets.set(target, box); + if (didObserve) { + this._observationTargets.set(target, box); + } else if (this._observationTargets.size === 0) { + // The target could not be observed (e.g. it is disconnected). Do not + // record the observation, so a later `observe` call can retry, and + // release the observer registration if it is not observing anything. + ResizeObserverManager.unregisterObserver(resizeObserverId); + this._resizeObserverId = null; + } } /** diff --git a/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js b/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js index f5879f494e29..38ed944556aa 100644 --- a/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js +++ b/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js @@ -24,6 +24,7 @@ import type ResizeObserver, { ResizeObserverCallback, } from '../ResizeObserver'; import type ResizeObserverEntry from '../ResizeObserverEntry'; +import type {NativeResizeObserverToken} from '../specs/NativeResizeObserver'; import {trace} from '../../../../../Libraries/Performance/Systrace'; import { @@ -67,6 +68,13 @@ function setTargetForInstanceHandle( instanceHandleToTargetMap.set(key, target); } +// We need to keep the token returned by native when starting an observation +// because targets lose their reference to the shadow node when they are +// unmounted, and without it we could not clean up the native observation +// after that point. +const targetToTokenMap: WeakMap = + new WeakMap(); + /** * Registers the given resize observer and returns a unique ID for it, which * is required to start observing targets. @@ -101,6 +109,8 @@ export function unregisterObserver(resizeObserverId: ResizeObserverId): void { * Starts observing a target on a specific resize observer. * If this is the first target being observed, this also sets up the * centralized notification callback in native. + * Returns `true` if the native observation was actually set up, or `false` + * if the target could not be observed (e.g. because it is disconnected). */ export function observe({ resizeObserverId, @@ -110,10 +120,10 @@ export function observe({ resizeObserverId: ResizeObserverId, target: ReactNativeElement, box: ResizeObserverBoxOptions, -}): void { +}): boolean { if (NativeResizeObserver == null) { throwIfNoNativeResizeObserver(); - return; + return false; } const registeredObserver = registeredResizeObservers.get(resizeObserverId); @@ -121,13 +131,13 @@ export function observe({ console.error( `ResizeObserverManager: could not start observing target because ResizeObserver with ID ${resizeObserverId} was not registered.`, ); - return; + return false; } const targetNativeNodeReference = getNativeNodeReference(target); if (targetNativeNodeReference == null) { // The target is disconnected. We can't observe it anymore. - return; + return false; } const instanceHandle = getInstanceHandle(target); @@ -135,7 +145,7 @@ export function observe({ console.error( 'ResizeObserverManager: could not find reference to instance handle from target', ); - return; + return false; } // Store the mapping between the instance handle and the target so we can @@ -147,11 +157,14 @@ export function observe({ isConnected = true; } - NativeResizeObserver.observe({ + const token = NativeResizeObserver.observe({ resizeObserverId, targetShadowNode: targetNativeNodeReference, box, }); + targetToTokenMap.set(target, token); + + return true; } /** @@ -175,14 +188,15 @@ export function unobserve( return; } - const targetNativeNodeReference = getNativeNodeReference(target); - if (targetNativeNodeReference == null) { - // The target is already disconnected, so native doesn't have anything - // to unobserve. + const targetToken = targetToTokenMap.get(target); + if (targetToken == null) { + console.error( + 'ResizeObserverManager: could not find registration data for target', + ); return; } - NativeResizeObserver.unobserve(resizeObserverId, targetNativeNodeReference); + NativeResizeObserver.unobserve(resizeObserverId, targetToken); } /** diff --git a/packages/react-native/src/private/webapis/resizeobserver/specs/NativeResizeObserver.js b/packages/react-native/src/private/webapis/resizeobserver/specs/NativeResizeObserver.js index 3175f942cc5a..7ab0c124ea44 100644 --- a/packages/react-native/src/private/webapis/resizeobserver/specs/NativeResizeObserver.js +++ b/packages/react-native/src/private/webapis/resizeobserver/specs/NativeResizeObserver.js @@ -28,11 +28,15 @@ export type NativeResizeObserverObserveOptions = { box?: ?string, }; +export opaque type NativeResizeObserverToken = unknown; + export interface Spec extends TurboModule { - readonly observe: (options: NativeResizeObserverObserveOptions) => void; + readonly observe: ( + options: NativeResizeObserverObserveOptions, + ) => NativeResizeObserverToken; readonly unobserve: ( resizeObserverId: number, - targetShadowNode: unknown, + targetToken: NativeResizeObserverToken, ) => void; readonly connect: (notifyResizeObserversFunction: () => void) => void; readonly disconnect: () => void; From bff14943e269fc7348ac990155563731d43ee4d5 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Thu, 23 Jul 2026 13:27:08 +0200 Subject: [PATCH 16/39] fix: improve unobserve logic --- .../resize/ResizeObserverManager.cpp | 77 ++++++++++++++----- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp index e3179a3847e9..a7b75d3283d0 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp @@ -60,29 +60,70 @@ void ResizeObserverManager::unobserve( const ShadowNodeFamily::Shared& shadowNodeFamily) { TraceSection s("ResizeObserverManager::unobserve"); - std::unique_lock lock(observersMutex_); - auto surfaceId = shadowNodeFamily->getSurfaceId(); - auto observersIt = observersBySurfaceId_.find(surfaceId); - if (observersIt == observersBySurfaceId_.end()) { - return; - } + // Whether another observer still targets the same family after this + // removal. If so, we must not drop the family's pending dirty state. + bool familyStillObserved = false; + { + std::unique_lock lock(observersMutex_); + + auto observersIt = observersBySurfaceId_.find(surfaceId); + if (observersIt == observersBySurfaceId_.end()) { + return; + } - auto& observers = observersIt->second; + auto& observers = observersIt->second; + + observers.erase( + std::remove_if( + observers.begin(), + observers.end(), + [resizeObserverId, &shadowNodeFamily](const auto& observer) { + return observer->getResizeObserverId() == resizeObserverId && + observer->getTargetShadowNodeFamily() == shadowNodeFamily; + }), + observers.end()); + + for (const auto& observer : observers) { + if (observer->getTargetShadowNodeFamily() == shadowNodeFamily) { + familyStillObserved = true; + break; + } + } - observers.erase( - std::remove_if( - observers.begin(), - observers.end(), - [resizeObserverId, &shadowNodeFamily](const auto& observer) { - return observer->getResizeObserverId() == resizeObserverId && - observer->getTargetShadowNodeFamily() == shadowNodeFamily; - }), - observers.end()); + if (observers.empty()) { + observersBySurfaceId_.erase(surfaceId); + } + } - if (observers.empty()) { - observersBySurfaceId_.erase(surfaceId); + // Drop bookkeeping for the removed observation so we don't run a wasted + // observation pass for it, deliver a stale entry after `unobserve`, or + // retain a dangling pointer to its (now potentially freed) family. Only + // clear the dirty family if no remaining observer still targets it. + if (!familyStillObserved) { + std::unique_lock lock(dirtyFamiliesMutex_); + auto dirtyFamiliesIt = dirtyFamiliesBySurfaceId_.find(surfaceId); + if (dirtyFamiliesIt != dirtyFamiliesBySurfaceId_.end()) { + dirtyFamiliesIt->second.erase(shadowNodeFamily.get()); + if (dirtyFamiliesIt->second.empty()) { + dirtyFamiliesBySurfaceId_.erase(dirtyFamiliesIt); + } + } + } + + { + std::unique_lock lock(pendingEntriesMutex_); + pendingEntries_.erase( + std::remove_if( + pendingEntries_.begin(), + pendingEntries_.end(), + [resizeObserverId, &shadowNodeFamily](const ResizeObserverEntry& + entry) { + return entry.resizeObserverId == resizeObserverId && + entry.shadowNodeFamily == shadowNodeFamily; + }), + pendingEntries_.end()); } } From 23f64bc1ddc48a3ca3544a6db48769c54bc82807 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Thu, 23 Jul 2026 13:33:03 +0200 Subject: [PATCH 17/39] fix: round device-pixel-content-box dimensions --- .../renderer/observers/resize/ResizeObserver.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp index 3d35b90d3319..8723e7728fb2 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace facebook::react { @@ -162,9 +163,15 @@ std::optional ResizeObserver::updateStateIfNeeded( layoutMetrics.borderWidth.top}, .size = contentBoxSize}; + // Per spec, the device-pixel-content-box must contain integer values. + // Round each axis independently: unlike Yoga's edge-snapping, we don't have + // a pixel-snapped content-box origin to tile against (insets come out of + // Yoga unsnapped and the origin is an accumulated sum of rounded relative + // offsets), and a single observed element has no sibling to align with, so + // rounding the dimension is the correct best-effort here. auto devicePixelContentBoxSize = Size{ - contentBoxSize.width * layoutMetrics.pointScaleFactor, - contentBoxSize.height * layoutMetrics.pointScaleFactor}; + std::round(contentBoxSize.width * layoutMetrics.pointScaleFactor), + std::round(contentBoxSize.height * layoutMetrics.pointScaleFactor)}; auto observedSize = getObservedSize( boxOptions_, borderBoxSize, contentBoxSize, devicePixelContentBoxSize); From 7bf5f85a5d35159b6dc3f58882e037c76e1a3336 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Thu, 23 Jul 2026 14:12:01 +0200 Subject: [PATCH 18/39] fix: frozing ResizeObserverEntry getters --- .../webapis/resizeobserver/ResizeObserver.js | 4 +- .../resizeobserver/ResizeObserverEntry.js | 58 +++++++++++++------ 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js index ace52ab31870..1688b731460d 100644 --- a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js +++ b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserver.js @@ -93,8 +93,8 @@ export default class ResizeObserver { const box = normalizeBoxOption(options?.box); if (this._observationTargets.has(target)) { - // Per spec, re-observing replaces the previous observation (equivalent - // to unobserve + observe) and must update native state. + // Re-observing a target already observed with the SAME box is a no-op + // and does not re-deliver. const previousBox = this._observationTargets.get(target); if (previousBox === box) { return; diff --git a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverEntry.js b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverEntry.js index f1dbb41ddb1d..ee38952fa988 100644 --- a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverEntry.js +++ b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverEntry.js @@ -31,6 +31,10 @@ export default class ResizeObserverEntry { // handle in the native entry (when the target is detached), so we need to // keep a reference to it directly. _target: ReactNativeElement; + _contentRect: ?DOMRectReadOnly; + _borderBoxSize: ?ReadonlyArray; + _contentBoxSize: ?ReadonlyArray; + _devicePixelContentBoxSize: ?ReadonlyArray; constructor( nativeEntry: NativeResizeObserverEntry, @@ -55,13 +59,16 @@ export default class ResizeObserverEntry { * bounds of its content box). */ get contentRect(): DOMRectReadOnly { - const contentRect = this._nativeEntry.contentRect; - return new DOMRectReadOnly( - contentRect[0], - contentRect[1], - contentRect[2], - contentRect[3], - ); + if (this._contentRect == null) { + const contentRect = this._nativeEntry.contentRect; + this._contentRect = new DOMRectReadOnly( + contentRect[0], + contentRect[1], + contentRect[2], + contentRect[3], + ); + } + return this._contentRect; } /** @@ -71,8 +78,13 @@ export default class ResizeObserverEntry { * always contain a single value. */ get borderBoxSize(): ReadonlyArray { - const borderBoxSize = this._nativeEntry.borderBoxSize; - return [new ResizeObserverSize(borderBoxSize[0], borderBoxSize[1])]; + if (this._borderBoxSize == null) { + const borderBoxSize = this._nativeEntry.borderBoxSize; + this._borderBoxSize = Object.freeze([ + new ResizeObserverSize(borderBoxSize[0], borderBoxSize[1]), + ]); + } + return this._borderBoxSize; } /** @@ -82,8 +94,13 @@ export default class ResizeObserverEntry { * always contain a single value. */ get contentBoxSize(): ReadonlyArray { - const contentBoxSize = this._nativeEntry.contentBoxSize; - return [new ResizeObserverSize(contentBoxSize[0], contentBoxSize[1])]; + if (this._contentBoxSize == null) { + const contentBoxSize = this._nativeEntry.contentBoxSize; + this._contentBoxSize = Object.freeze([ + new ResizeObserverSize(contentBoxSize[0], contentBoxSize[1]), + ]); + } + return this._contentBoxSize; } /** @@ -93,14 +110,17 @@ export default class ResizeObserverEntry { * always contain a single value. */ get devicePixelContentBoxSize(): ReadonlyArray { - const devicePixelContentBoxSize = - this._nativeEntry.devicePixelContentBoxSize; - return [ - new ResizeObserverSize( - devicePixelContentBoxSize[0], - devicePixelContentBoxSize[1], - ), - ]; + if (this._devicePixelContentBoxSize == null) { + const devicePixelContentBoxSize = + this._nativeEntry.devicePixelContentBoxSize; + this._devicePixelContentBoxSize = Object.freeze([ + new ResizeObserverSize( + devicePixelContentBoxSize[0], + devicePixelContentBoxSize[1], + ), + ]); + } + return this._devicePixelContentBoxSize; } } From 41730a7eace8419c7fd2383735ca3db4ba5398f0 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Thu, 23 Jul 2026 14:35:24 +0200 Subject: [PATCH 19/39] refactor: simplify runResizeObservations method signature --- .../renderer/observers/resize/ResizeObserverManager.cpp | 4 +--- .../renderer/observers/resize/ResizeObserverManager.h | 4 +--- .../RuntimeSchedulerResizeObserverDelegate.h | 7 +------ .../renderer/runtimescheduler/RuntimeScheduler_Modern.cpp | 3 +-- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp index a7b75d3283d0..ca43798c43a9 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp @@ -230,9 +230,7 @@ void ResizeObserverManager::shadowTreeDidCommit( #pragma mark - RuntimeSchedulerResizeObserverDelegate -void ResizeObserverManager::runResizeObservations( - const std::unordered_set< - SurfaceId>& /*surfaceIdsWithPendingRenderingUpdates*/) { +void ResizeObserverManager::runResizeObservations() { TraceSection s("ResizeObserverManager::runResizeObservations"); // Drain the dirty families collected by the commit hook since the last diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h index c0e2e17a090e..f7b595a2040b 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h @@ -51,9 +51,7 @@ class ResizeObserverManager final #pragma mark - RuntimeSchedulerResizeObserverDelegate - void runResizeObservations( - const std::unordered_set& - surfaceIdsWithPendingRenderingUpdates) override; + void runResizeObservations() override; #pragma mark - UIManagerCommitHook diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeSchedulerResizeObserverDelegate.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeSchedulerResizeObserverDelegate.h index d5aaf5c5c514..43a2f3a5109f 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeSchedulerResizeObserverDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeSchedulerResizeObserverDelegate.h @@ -7,18 +7,13 @@ #pragma once -#include - namespace facebook::react { -using SurfaceId = int32_t; - class RuntimeSchedulerResizeObserverDelegate { public: virtual ~RuntimeSchedulerResizeObserverDelegate() = default; - virtual void runResizeObservations( - const std::unordered_set &surfaceIdsWithPendingRenderingUpdates) = 0; + virtual void runResizeObservations() = 0; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp index f5dc4eb72aa4..da4289f45a64 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp @@ -366,8 +366,7 @@ void RuntimeScheduler_Modern::updateRendering(HighResTimeStamp taskEndTime) { // See // https://w3c.github.io/csswg-drafts/resize-observer/#broadcast-resize-notifications-h if (resizeObserverDelegate_ != nullptr) { - resizeObserverDelegate_->runResizeObservations( - surfaceIdsWithPendingRenderingUpdates_); + resizeObserverDelegate_->runResizeObservations(); } // This is the integration of the Intersection Observer API in the Event Loop. From 3afaeae1740c954857c36530e6d5f76aec73d9ae Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Fri, 24 Jul 2026 14:35:40 +0200 Subject: [PATCH 20/39] feat: add observation sequence to ResizeObserver for ordered delivery --- .../observers/resize/ResizeObserver.cpp | 6 ++- .../observers/resize/ResizeObserver.h | 10 ++++- .../resize/ResizeObserverManager.cpp | 44 ++++++++++++++++--- .../observers/resize/ResizeObserverManager.h | 5 +++ .../internals/ResizeObserverManager.js | 13 +++++- 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp index 8723e7728fb2..8fc31798b8ac 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp @@ -81,10 +81,12 @@ ResizeObserverEntry makeResizeObserverEntry( ResizeObserver::ResizeObserver( ResizeObserverObserverId resizeObserverId, ShadowNodeFamily::Shared targetShadowNodeFamily, - ResizeObserverBoxOptions boxOptions) + ResizeObserverBoxOptions boxOptions, + uint64_t observationSequence) : resizeObserverId_(resizeObserverId), targetShadowNodeFamily_(std::move(targetShadowNodeFamily)), - boxOptions_(boxOptions) {} + boxOptions_(boxOptions), + observationSequence_(observationSequence) {} // Partially equivalent to // https://w3c.github.io/csswg-drafts/resize-observer/#broadcast-active-resize-observations diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h index 86405045d603..bdb5fa4b420e 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -46,7 +47,8 @@ class ResizeObserver { ResizeObserver( ResizeObserverObserverId resizeObserverId, ShadowNodeFamily::Shared targetShadowNodeFamily, - ResizeObserverBoxOptions boxOptions); + ResizeObserverBoxOptions boxOptions, + uint64_t observationSequence); // Partially equivalent to // https://w3c.github.io/csswg-drafts/resize-observer/#broadcast-active-resize-observations @@ -65,6 +67,11 @@ class ResizeObserver { return boxOptions_; } + // Monotonic order of `observe()` registration for this observation. + uint64_t getObservationSequence() const { + return observationSequence_; + } + // Whether this observation still needs its first delivery at the next // `runResizeObservations` step (just registered, or target was removed). bool needsInitialDeliveryCheck() const { @@ -75,6 +82,7 @@ class ResizeObserver { ResizeObserverObserverId resizeObserverId_; ShadowNodeFamily::Shared targetShadowNodeFamily_; ResizeObserverBoxOptions boxOptions_; + uint64_t observationSequence_; // Last delivered observed-box size. Empty until the first entry is produced. // Reset when the target leaves the tree so a reinsert is a new observation. diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp index ca43798c43a9..a12b6211dc37 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp @@ -27,6 +27,12 @@ RootShadowNode::Shared getRootShadowNode( return rootShadowNode; } + +struct PendingObservation { + ResizeObserverEntry entry; + ResizeObserverObserverId resizeObserverId; + uint64_t observationSequence; +}; } // namespace ResizeObserverManager::ResizeObserverManager() = default; @@ -52,7 +58,10 @@ void ResizeObserverManager::observe( auto& observers = observersBySurfaceId_[surfaceId]; observers.emplace_back(std::make_unique( - resizeObserverId, shadowNodeFamily, boxOptions)); + resizeObserverId, + shadowNodeFamily, + boxOptions, + nextObservationSequence_++)); } void ResizeObserverManager::unobserve( @@ -266,7 +275,7 @@ void ResizeObserverManager::runResizeObservations() { return; } - std::vector entries; + std::vector pendingObservations; for (auto surfaceId : candidateSurfaceIds) { RootShadowNode::Shared rootShadowNode = @@ -299,19 +308,42 @@ void ResizeObserverManager::runResizeObservations() { auto entry = observer->updateStateIfNeeded(*rootShadowNode); if (entry) { - entries.push_back(std::move(entry).value()); + pendingObservations.push_back(PendingObservation{ + .entry = std::move(entry).value(), + .resizeObserverId = observer->getResizeObserverId(), + .observationSequence = observer->getObservationSequence(), + }); } } } - if (entries.empty()) { + if (pendingObservations.empty()) { return; } + // Deliver in the order the spec's algorithms imply by iterating their + // ordered lists: observers in registration order (`[[resizeObservers]]`), + // and within an observer its targets in `observe()` order + // (`[[observationTargets]]`). Depth is deliberately not a factor: in the + // spec it only gates the rounds of the (re-layout) broadcast loop, which + // RN does not run, and never reorders entries within a callback. + std::sort( + pendingObservations.begin(), + pendingObservations.end(), + [](const PendingObservation& a, const PendingObservation& b) { + if (a.resizeObserverId != b.resizeObserverId) { + return a.resizeObserverId < b.resizeObserverId; + } + return a.observationSequence < b.observationSequence; + }); + { std::unique_lock lock(pendingEntriesMutex_); - pendingEntries_.insert( - pendingEntries_.end(), entries.begin(), entries.end()); + pendingEntries_.reserve( + pendingEntries_.size() + pendingObservations.size()); + for (auto& observation : pendingObservations) { + pendingEntries_.push_back(std::move(observation.entry)); + } } notifyObserversIfNecessary(); diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h index f7b595a2040b..b919f5974025 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -70,6 +71,10 @@ class ResizeObserverManager final observersBySurfaceId_; mutable std::mutex observersMutex_; + // Monotonic counter assigned at `observe()` so deliveries can preserve + // observation registration order. + uint64_t nextObservationSequence_{0}; + // Families that went dirty (i.e. were part of `affectedLayoutableNodes`) // since the last time `runResizeObservations` drained this map. Populated // by the commit hook (potentially off the JS thread) and drained by the diff --git a/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js b/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js index 38ed944556aa..60427e6ba03d 100644 --- a/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js +++ b/packages/react-native/src/private/webapis/resizeobserver/internals/ResizeObserverManager.js @@ -238,7 +238,18 @@ function doNotifyResizeObservers(): void { list.push(createResizeObserverEntry(nativeEntry, target)); } - for (const [resizeObserverId, entriesForObserver] of entriesByObserver) { + // Native delivers entries sorted by observer registration order; keep that + // when invoking callbacks (Map insertion order alone is not enough if + // batches were merged out of order). + const observerIds = Array.from(entriesByObserver.keys()).sort( + (a, b) => a - b, + ); + for (const resizeObserverId of observerIds) { + const entriesForObserver = entriesByObserver.get(resizeObserverId); + if (entriesForObserver == null) { + continue; + } + const registeredObserver = registeredResizeObservers.get(resizeObserverId); if (!registeredObserver) { // This could happen if the observer is disconnected between commit From 2df8f1e49a597873819e0a47174d343974598c07 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 27 Jul 2026 14:32:02 +0200 Subject: [PATCH 21/39] feat: handle unmount 0x0 case --- .../observers/resize/ResizeObserver.cpp | 41 ++++++++++++----- .../observers/resize/ResizeObserver.h | 13 +++++- .../resize/ResizeObserverManager.cpp | 44 +++++++++++++------ .../observers/resize/ResizeObserverManager.h | 6 +++ 4 files changed, 80 insertions(+), 24 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp index 8fc31798b8ac..0f870c051e4e 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.cpp @@ -93,20 +93,41 @@ ResizeObserver::ResizeObserver( std::optional ResizeObserver::updateStateIfNeeded( const RootShadowNode& rootShadowNode) { auto ancestors = targetShadowNodeFamily_->getAncestors(rootShadowNode); + const auto* targetShadowNode = + ancestors.empty() ? nullptr : getTargetShadowNode(ancestors); - if (ancestors.empty()) { - // The target is not part of the tree. If it is later reinserted, treat it - // as a brand new observation. - lastReportedSize_.reset(); - return std::nullopt; - } - - const auto* targetShadowNode = getTargetShadowNode(ancestors); if (targetShadowNode == nullptr) { - lastReportedSize_.reset(); - return std::nullopt; + // The target left the tree (its component was unmounted or removed). Per + // spec and browser behavior, removal from the tree fires one final + // notification with a zero-sized box (like `display: none`). We keep + // `lastReportedSize_` set to that 0x0 so we don't re-deliver it, and mark + // the observation as detached so it is not re-checked every commit; a + // later reinsertion arrives via the "dirty family" path and clears this. + Size zeroSize{0, 0}; + auto observedSize = + getObservedSize(boxOptions_, zeroSize, zeroSize, zeroSize); + + const bool alreadyDelivered = + lastReportedSize_.has_value() && lastReportedSize_.value() == observedSize; + detached_ = true; + if (alreadyDelivered) { + return std::nullopt; + } + + lastReportedSize_ = observedSize; + + return makeResizeObserverEntry( + resizeObserverId_, + targetShadowNodeFamily_, + zeroSize, + zeroSize, + zeroSize, + Rect{.origin = {0, 0}, .size = zeroSize}); } + // The target is attached; any previous detached state no longer applies. + detached_ = false; + const bool isInitialDelivery = !lastReportedSize_.has_value(); // Per spec (and browser behavior), `display: none` triggers a notification diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h index bdb5fa4b420e..8681da6072fc 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserver.h @@ -78,6 +78,13 @@ class ResizeObserver { return !lastReportedSize_.has_value(); } + // Whether the target left the tree and its final 0x0 entry has already been + // delivered. Such observations stay quiet (no re-checks) until the target is + // reinserted, which is picked up via the normal "dirty family" commit path. + bool hasDeliveredDetachedState() const { + return detached_ && lastReportedSize_.has_value(); + } + private: ResizeObserverObserverId resizeObserverId_; ShadowNodeFamily::Shared targetShadowNodeFamily_; @@ -85,8 +92,12 @@ class ResizeObserver { uint64_t observationSequence_; // Last delivered observed-box size. Empty until the first entry is produced. - // Reset when the target leaves the tree so a reinsert is a new observation. std::optional lastReportedSize_; + + // True once the target has been observed detached from the tree and its + // final 0x0 entry delivered. Cleared as soon as the target is found attached + // again. + bool detached_{false}; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp index a12b6211dc37..7561807c8ade 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.cpp @@ -228,13 +228,15 @@ void ResizeObserverManager::shadowTreeDidCommit( } } - if (newlyDirtyFamilies.empty()) { - return; - } - std::unique_lock lock(dirtyFamiliesMutex_); - auto& dirtyFamilies = dirtyFamiliesBySurfaceId_[surfaceId]; - dirtyFamilies.insert(newlyDirtyFamilies.begin(), newlyDirtyFamilies.end()); + // Always record the commit, even when no observed family changed layout, so + // `runResizeObservations` re-checks this surface's observers for targets that + // left the tree (removals are not reported in `affectedLayoutableNodes`). + committedSurfaceIds_.insert(surfaceId); + if (!newlyDirtyFamilies.empty()) { + auto& dirtyFamilies = dirtyFamiliesBySurfaceId_[surfaceId]; + dirtyFamilies.insert(newlyDirtyFamilies.begin(), newlyDirtyFamilies.end()); + } } #pragma mark - RuntimeSchedulerResizeObserverDelegate @@ -246,16 +248,19 @@ void ResizeObserverManager::runResizeObservations() { // time this step ran. std::unordered_map> dirtyFamiliesBySurfaceId; + std::unordered_set committedSurfaceIds; { std::unique_lock lock(dirtyFamiliesMutex_); dirtyFamiliesBySurfaceId.swap(dirtyFamiliesBySurfaceId_); + committedSurfaceIds.swap(committedSurfaceIds_); } - // A surface is relevant to this step if either (a) it has dirty families - // collected from a commit, or (b) it has observers awaiting their first - // delivery check (e.g. observers just registered via `observe()`). + // A surface is relevant to this step if either (a) it committed since the + // last pass (which covers both layout changes and target removals), or + // (b) it has observers awaiting their first delivery check (e.g. observers + // just registered via `observe()`). std::unordered_set candidateSurfaceIds; - for (const auto& [surfaceId, families] : dirtyFamiliesBySurfaceId) { + for (auto surfaceId : committedSurfaceIds) { candidateSurfaceIds.insert(surfaceId); } @@ -290,6 +295,8 @@ void ResizeObserverManager::runResizeObservations() { ? &dirtyFamiliesIt->second : nullptr; + bool surfaceCommitted = committedSurfaceIds.contains(surfaceId); + std::unique_lock lock(observersMutex_); auto observersIt = observersBySurfaceId_.find(surfaceId); @@ -298,11 +305,22 @@ void ResizeObserverManager::runResizeObservations() { } for (auto& observer : observersIt->second) { - // Only recompute observers whose target was affected by a commit - // since the last pass, or that still need their first delivery check. bool wasAffected = dirtyFamilies != nullptr && dirtyFamilies->contains(observer->getTargetShadowNodeFamily().get()); - if (!wasAffected && !observer->needsInitialDeliveryCheck()) { + + // Re-check a delivered observation whenever its surface committed, to + // catch a target that left the tree (removals don't show up as dirty + // families) and deliver its final 0x0 entry. Skip observations already + // settled as detached: their reinsertion arrives via the dirty path. + bool maybeDetached = surfaceCommitted && + !observer->needsInitialDeliveryCheck() && + !observer->hasDeliveredDetachedState(); + + // Only recompute observers whose target was affected by a commit since + // the last pass, that still need their first delivery check, or that + // may have just detached. + if (!wasAffected && !observer->needsInitialDeliveryCheck() && + !maybeDetached) { continue; } diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h index b919f5974025..4a1226a5a161 100644 --- a/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/ResizeObserverManager.h @@ -81,6 +81,12 @@ class ResizeObserverManager final // event-loop step (always on the JS thread). std::unordered_map> dirtyFamiliesBySurfaceId_; + // Surfaces that committed since the last `runResizeObservations` and have at + // least one observer. Used to re-check already-delivered observers for + // targets that left the tree: removals never appear in + // `affectedLayoutableNodes`, so they can't be detected via dirty families. + // Guarded by the same mutex as `dirtyFamiliesBySurfaceId_`. + std::unordered_set committedSurfaceIds_; std::mutex dirtyFamiliesMutex_; std::function notifyResizeObserversFunction_; From d0ca2281f1215d81498bc15e043c57a1f3605714 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 27 Jul 2026 14:32:47 +0200 Subject: [PATCH 22/39] feat: add ResizeObserver Fantom tests --- .../react/runtime/CMakeLists.txt | 1 + .../__tests__/ResizeObserver-itest.js | 1260 +++++++++++++++++ .../react-native-fantom/tester/CMakeLists.txt | 4 + 3 files changed, 1265 insertions(+) create mode 100644 packages/react-native/src/private/webapis/resizeobserver/__tests__/ResizeObserver-itest.js diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/CMakeLists.txt b/packages/react-native/ReactCxxPlatform/react/runtime/CMakeLists.txt index 01bf276bb49d..088c1016c4d1 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/CMakeLists.txt +++ b/packages/react-native/ReactCxxPlatform/react/runtime/CMakeLists.txt @@ -41,6 +41,7 @@ target_link_libraries(react_cxx_platform_react_runtime react_nativemodule_defaults react_nativemodule_intersectionobserver react_nativemodule_mutationobserver + react_nativemodule_resizeobserver react_nativemodule_webperformance react_renderer_graphics react_renderer_runtimescheduler diff --git a/packages/react-native/src/private/webapis/resizeobserver/__tests__/ResizeObserver-itest.js b/packages/react-native/src/private/webapis/resizeobserver/__tests__/ResizeObserver-itest.js new file mode 100644 index 000000000000..371a4d2e17b2 --- /dev/null +++ b/packages/react-native/src/private/webapis/resizeobserver/__tests__/ResizeObserver-itest.js @@ -0,0 +1,1260 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @fantom_flags enableResizeObserverByDefault:true + * @flow strict-local + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {HostInstance} from 'react-native'; +import type ResizeObserverType from 'react-native/src/private/webapis/resizeobserver/ResizeObserver'; +import type ResizeObserverEntryType from 'react-native/src/private/webapis/resizeobserver/ResizeObserverEntry'; +import type ResizeObserverSizeType from 'react-native/src/private/webapis/resizeobserver/ResizeObserverSize'; + +import ensureInstance from '../../../__tests__/utilities/ensureInstance'; +import {createShadowNodeReferenceCountingRef} from '../../../__tests__/utilities/ShadowNodeReferenceCounter'; +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import {createRef} from 'react'; +import {View} from 'react-native'; +import setUpResizeObserver from 'react-native/src/private/setup/setUpResizeObserver'; +import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement'; +import DOMRectReadOnly from 'react-native/src/private/webapis/geometry/DOMRectReadOnly'; + +declare const ResizeObserver: Class; +declare const ResizeObserverEntry: Class; +declare const ResizeObserverSize: Class; + +setUpResizeObserver(); + +function ensureReactNativeElement(value: unknown): ReactNativeElement { + return ensureInstance(value, ReactNativeElement); +} + +function expectEntrySizes( + entry: ResizeObserverEntry, + expected: { + contentWidth: number, + contentHeight: number, + borderWidth: number, + borderHeight: number, + contentX?: number, + contentY?: number, + devicePixelWidth?: number, + devicePixelHeight?: number, + }, +): void { + expect(entry.contentRect).toBeInstanceOf(DOMRectReadOnly); + expect(entry.contentRect.x).toBe(expected.contentX ?? 0); + expect(entry.contentRect.y).toBe(expected.contentY ?? 0); + expect(entry.contentRect.width).toBe(expected.contentWidth); + expect(entry.contentRect.height).toBe(expected.contentHeight); + + expect(entry.contentBoxSize).toHaveLength(1); + expect(entry.contentBoxSize[0].inlineSize).toBe(expected.contentWidth); + expect(entry.contentBoxSize[0].blockSize).toBe(expected.contentHeight); + + expect(entry.borderBoxSize).toHaveLength(1); + expect(entry.borderBoxSize[0].inlineSize).toBe(expected.borderWidth); + expect(entry.borderBoxSize[0].blockSize).toBe(expected.borderHeight); + + if (expected.devicePixelWidth != null && expected.devicePixelHeight != null) { + expect(entry.devicePixelContentBoxSize).toHaveLength(1); + expect(entry.devicePixelContentBoxSize[0].inlineSize).toBe( + expected.devicePixelWidth, + ); + expect(entry.devicePixelContentBoxSize[0].blockSize).toBe( + expected.devicePixelHeight, + ); + } +} + +describe('ResizeObserver', () => { + let observer: ResizeObserver; + + afterEach(() => { + Fantom.runTask(() => { + if (observer != null) { + observer.disconnect(); + } + }); + }); + + describe('constructor(callback)', () => { + it('should throw if `callback` is not provided', () => { + expect(() => { + // $FlowExpectedError[incompatible-type] + return new ResizeObserver(); + }).toThrow( + "Failed to construct 'ResizeObserver': 1 argument required, but only 0 present.", + ); + }); + + it('should throw if `callback` is not a function', () => { + expect(() => { + // $FlowExpectedError[incompatible-type] + return new ResizeObserver('not a function!'); + }).toThrow( + "Failed to construct 'ResizeObserver': parameter 1 is not of type 'Function'.", + ); + }); + }); + + describe('observe(target, options)', () => { + it('should throw if `target` is null or undefined', () => { + observer = new ResizeObserver(() => {}); + + expect(() => { + // $FlowExpectedError[incompatible-type] + observer.observe(null); + }).toThrow( + "Failed to execute 'observe' on 'ResizeObserver': parameter 1 is null or undefined.", + ); + + expect(() => { + // $FlowExpectedError[incompatible-type] + observer.observe(undefined); + }).toThrow( + "Failed to execute 'observe' on 'ResizeObserver': parameter 1 is null or undefined.", + ); + }); + + it('should throw if `target` is not a `ReactNativeElement`', () => { + observer = new ResizeObserver(() => {}); + expect(() => { + // $FlowExpectedError[incompatible-type] + observer.observe('something'); + }).toThrow( + "Failed to execute 'observe' on 'ResizeObserver': parameter 1 is not of type 'ReactNativeElement'.", + ); + }); + + it('should throw if `box` is not a valid enum value', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + const node = ensureReactNativeElement(nodeRef.current); + + observer = new ResizeObserver(() => {}); + expect(() => { + // $FlowExpectedError[incompatible-type] + observer.observe(node, {box: 'margin-box'}); + }).toThrow( + "Failed to execute 'observe' on 'ResizeObserver': Failed to read the 'box' property from 'ResizeObserverOptions': The provided value 'margin-box' is not a valid enum value of type ResizeObserverBoxOptions.", + ); + }); + + it('should accept valid `box` option values', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + const node = ensureReactNativeElement(nodeRef.current); + + expect(() => { + observer = new ResizeObserver(() => {}); + observer.observe(node, {box: 'content-box'}); + observer.unobserve(node); + observer.observe(node, {box: 'border-box'}); + observer.unobserve(node); + observer.observe(node, {box: 'device-pixel-content-box'}); + }).not.toThrow(); + }); + + it('should ignore calls to observe disconnected targets', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + + Fantom.runTask(() => { + root.render(<>); + }); + expect(node.isConnected).toBe(false); + + const callback = jest.fn(); + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + expect(() => { + observer.observe(node); + }).not.toThrow(); + }); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('should deliver an initial observation for a sized target', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot({devicePixelRatio: 2}); + + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node); + }); + + expect(callback).toHaveBeenCalledTimes(1); + const [entries, reportedObserver] = callback.mock.lastCall; + expect(entries).toHaveLength(1); + expect(entries[0]).toBeInstanceOf(ResizeObserverEntry); + expect(entries[0].target).toBe(node); + expect(reportedObserver).toBe(observer); + expectEntrySizes(entries[0], { + contentWidth: 100, + contentHeight: 50, + borderWidth: 100, + borderHeight: 50, + devicePixelWidth: 200, + devicePixelHeight: 100, + }); + }); + + it('should deliver an initial observation for a zero-sized target', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node); + }); + + expect(callback).toHaveBeenCalledTimes(1); + const [entries] = callback.mock.lastCall; + expectEntrySizes(entries[0], { + contentWidth: 0, + contentHeight: 0, + borderWidth: 0, + borderHeight: 0, + }); + }); + + it('should report content and border box sizes with padding and border', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node); + }); + + expect(callback).toHaveBeenCalledTimes(1); + const [entries] = callback.mock.lastCall; + // border-box: 100x80 + // content insets = padding(10) + border(5) on each side → content 70x50 + // contentRect origin is padding only → (10, 10) + expectEntrySizes(entries[0], { + contentWidth: 70, + contentHeight: 50, + borderWidth: 100, + borderHeight: 80, + contentX: 10, + contentY: 10, + }); + }); + + it('should report device-pixel-content-box observations using the root pixel ratio', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot({devicePixelRatio: 3}); + + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node, {box: 'device-pixel-content-box'}); + }); + + expect(callback).toHaveBeenCalledTimes(1); + const [entries] = callback.mock.lastCall; + expect(entries[0].devicePixelContentBoxSize[0].inlineSize).toBe( + Math.round(10.4 * 3), + ); + expect(entries[0].devicePixelContentBoxSize[0].blockSize).toBe( + Math.round(10.6 * 3), + ); + }); + + it('should ignore subsequent observe calls for the same target and box', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node); + }); + expect(callback).toHaveBeenCalledTimes(1); + + Fantom.runTask(() => { + observer.observe(node); + observer.observe(node, {box: 'content-box'}); + }); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should re-deliver when re-observing the same target with a different box', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node, {box: 'content-box'}); + }); + expect(callback).toHaveBeenCalledTimes(1); + + Fantom.runTask(() => { + observer.observe(node, {box: 'border-box'}); + }); + expect(callback).toHaveBeenCalledTimes(2); + + const [entries] = callback.mock.lastCall; + expect(entries[0].borderBoxSize[0].inlineSize).toBe(100); + expect(entries[0].borderBoxSize[0].blockSize).toBe(80); + expect(entries[0].contentBoxSize[0].inlineSize).toBe(70); + expect(entries[0].contentBoxSize[0].blockSize).toBe(50); + }); + + it('should report size updates for the observed target', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node); + }); + expect(callback).toHaveBeenCalledTimes(1); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(2); + const [entries] = callback.mock.lastCall; + expectEntrySizes(entries[0], { + contentWidth: 200, + contentHeight: 75, + borderWidth: 200, + borderHeight: 75, + }); + }); + + it('should not report updates that do not change the observed box', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node, {box: 'border-box'}); + }); + expect(callback).toHaveBeenCalledTimes(1); + + // Non-layout style change should not notify. + Fantom.runTask(() => { + root.render( + , + ); + }); + expect(callback).toHaveBeenCalledTimes(1); + + // Padding changes content-box but not border-box. + Fantom.runTask(() => { + root.render( + , + ); + }); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should report zero sizes when the target becomes hidden', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node); + }); + expect(callback).toHaveBeenCalledTimes(1); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(2); + const [entries] = callback.mock.lastCall; + expectEntrySizes(entries[0], { + contentWidth: 0, + contentHeight: 0, + borderWidth: 0, + borderHeight: 0, + }); + }); + + it('should report a content-box change when padding is added', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node, {box: 'content-box'}); + }); + expect(callback).toHaveBeenCalledTimes(1); + + // Adding padding shrinks the content box (border box is unchanged), so a + // content-box observation must fire. This is the complement of the + // border-box case, where the same padding change does not fire. + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(2); + expectEntrySizes(callback.mock.lastCall[0][0], { + contentWidth: 80, + contentHeight: 60, + borderWidth: 100, + borderHeight: 80, + contentX: 10, + contentY: 10, + }); + }); + + it('should report border-box changes when observing the border box', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node, {box: 'border-box'}); + }); + expect(callback).toHaveBeenCalledTimes(1); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(2); + expectEntrySizes(callback.mock.lastCall[0][0], { + contentWidth: 130, // 140 - 2 * 5 + contentHeight: 70, // 80 - 2 * 5 + borderWidth: 140, + borderHeight: 80, + }); + }); + + it('should not deliver a callback for transforms (layout size unchanged)', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node); + }); + expect(callback).toHaveBeenCalledTimes(1); + + // A CSS transform does not change the layout box, so no observation + // fires (matches the spec: observations are not triggered by transforms). + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should re-deliver the size when a hidden target is shown again', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node); + }); + expect(callback).toHaveBeenCalledTimes(1); + + Fantom.runTask(() => { + root.render( + , + ); + }); + expect(callback).toHaveBeenCalledTimes(2); + expectEntrySizes(callback.mock.lastCall[0][0], { + contentWidth: 0, + contentHeight: 0, + borderWidth: 0, + borderHeight: 0, + }); + + Fantom.runTask(() => { + root.render( + , + ); + }); + expect(callback).toHaveBeenCalledTimes(3); + expectEntrySizes(callback.mock.lastCall[0][0], { + contentWidth: 100, + contentHeight: 50, + borderWidth: 100, + borderHeight: 50, + }); + }); + + it('should deliver to multiple observers watching the same target', () => { + const nodeRef = createRef(); + let observer1: ResizeObserver; + let observer2: ResizeObserver; + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback1 = jest.fn(); + const callback2 = jest.fn(); + + Fantom.runTask(() => { + observer1 = new ResizeObserver(callback1); + observer2 = new ResizeObserver(callback2); + observer1.observe(node); + observer2.observe(node); + }); + + expect(callback1).toHaveBeenCalledTimes(1); + expect(callback2).toHaveBeenCalledTimes(1); + expect(callback1.mock.lastCall[0][0].target).toBe(node); + expect(callback2.mock.lastCall[0][0].target).toBe(node); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect(callback1).toHaveBeenCalledTimes(2); + expect(callback2).toHaveBeenCalledTimes(2); + + Fantom.runTask(() => { + observer1.disconnect(); + observer2.disconnect(); + }); + }); + + it('should deliver a final 0x0 observation when the target is removed from the tree', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node); + }); + expect(callback).toHaveBeenCalledTimes(1); + + // Unmount the observed target *without* calling `unobserve`. + Fantom.runTask(() => { + root.render(<>); + }); + + // Matches the Web: removal from the tree fires one final notification + // with a 0x0 box. + expect(callback).toHaveBeenCalledTimes(2); + const [entries] = callback.mock.lastCall; + expect(entries).toHaveLength(1); + expect(entries[0].target).toBe(node); + expectEntrySizes(entries[0], { + contentWidth: 0, + contentHeight: 0, + borderWidth: 0, + borderHeight: 0, + }); + + // No further notifications while it stays detached. + Fantom.runTask(() => { + root.render(); + }); + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('should report updates to the right observers', () => { + const node1Ref = createRef(); + const node2Ref = createRef(); + let observer1: ResizeObserver; + let observer2: ResizeObserver; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render( + <> + + + , + ); + }); + + const node1 = ensureReactNativeElement(node1Ref.current); + const node2 = ensureReactNativeElement(node2Ref.current); + const callback1 = jest.fn(); + const callback2 = jest.fn(); + + Fantom.runTask(() => { + observer1 = new ResizeObserver(callback1); + observer1.observe(node1); + observer1.observe(node2); + + observer2 = new ResizeObserver(callback2); + observer2.observe(node2); + }); + + expect(callback1).toHaveBeenCalledTimes(1); + expect(callback2).toHaveBeenCalledTimes(1); + + const [entries1, reportedObserver1] = callback1.mock.lastCall; + expect(reportedObserver1).toBe(observer1); + expect(entries1).toHaveLength(2); + expect(entries1[0].target).toBe(node1); + expect(entries1[1].target).toBe(node2); + + const [entries2, reportedObserver2] = callback2.mock.lastCall; + expect(reportedObserver2).toBe(observer2); + expect(entries2).toHaveLength(1); + expect(entries2[0].target).toBe(node2); + + Fantom.runTask(() => { + root.render( + <> + + + , + ); + }); + + expect(callback1).toHaveBeenCalledTimes(2); + expect(callback2).toHaveBeenCalledTimes(1); + const [updateEntries] = callback1.mock.lastCall; + expect(updateEntries).toHaveLength(1); + expect(updateEntries[0].target).toBe(node1); + expect(updateEntries[0].contentRect.width).toBe(60); + + Fantom.runTask(() => { + observer1.disconnect(); + observer2.disconnect(); + }); + }); + + describe('observing multiple targets in the same observer', () => { + it('should report changes for disjoint observations in observation order', () => { + const node1Ref = createRef(); + const node2Ref = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + <> + + + , + ); + }); + + const node1 = ensureReactNativeElement(node1Ref.current); + const node2 = ensureReactNativeElement(node2Ref.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node2); + observer.observe(node1); + }); + + expect(callback).toHaveBeenCalledTimes(1); + const [entries] = callback.mock.lastCall; + expect(entries.map(entry => entry.target)).toEqual([node2, node1]); + + Fantom.runTask(() => { + root.render( + <> + + + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(2); + const [updateEntries] = callback.mock.lastCall; + expect(updateEntries).toHaveLength(1); + expect(updateEntries[0].target).toBe(node2); + expect(updateEntries[0].contentRect.width).toBe(90); + }); + }); + + describe('memory handling', () => { + it('should not retain initial children of observed targets', () => { + const root = Fantom.createRoot(); + observer = new ResizeObserver(() => {}); + + const [getReferenceCount, ref] = createShadowNodeReferenceCountingRef(); + + const observeRef: React.RefSetter< + React.ElementRef, + > = instance => { + const element = ensureReactNativeElement(instance); + observer.observe(element); + return () => { + observer.unobserve(element); + }; + }; + + function Observe({children}: Readonly<{children?: React.Node}>) { + return ( + + {children} + + ); + } + + Fantom.runTask(() => { + root.render( + + + , + ); + }); + + expect(getReferenceCount()).toBeGreaterThan(0); + + Fantom.runTask(() => { + root.render(); + }); + + expect(getReferenceCount()).toBe(0); + }); + }); + }); + + describe('unobserve(target)', () => { + it('should throw if `target` is not a `ReactNativeElement`', () => { + observer = new ResizeObserver(() => {}); + expect(() => { + // $FlowExpectedError[incompatible-type] + observer.unobserve('something'); + }).toThrow( + "Failed to execute 'unobserve' on 'ResizeObserver': parameter 1 is not of type 'ReactNativeElement'.", + ); + }); + + it('should ignore the call if `target` was not observed (not fail)', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.unobserve(node); + }); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('should stop observing the target if it was observed', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node); + }); + expect(callback).toHaveBeenCalledTimes(1); + + Fantom.runTask(() => { + observer.unobserve(node); + }); + + Fantom.runTask(() => { + root.render( + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should stop observing the target if it was observed (detached target after observing)', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node); + }); + expect(callback).toHaveBeenCalledTimes(1); + + Fantom.runTask(() => { + root.render(<>); + }); + expect(node.isConnected).toBe(false); + // Removal from the tree delivers one final 0x0 observation (Web parity). + expect(callback).toHaveBeenCalledTimes(2); + + Fantom.runTask(() => { + expect(() => { + observer.unobserve(node); + }).not.toThrow(); + }); + // No further deliveries after unobserve. + expect(callback).toHaveBeenCalledTimes(2); + }); + + it('should not report the initial state if the target is unobserved before it is delivered', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node); + observer.unobserve(node); + }); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('should work with multiple resize observer instances', () => { + const nodeRef = createRef(); + let observer1: ResizeObserver; + let observer2: ResizeObserver; + + const root = Fantom.createRoot(); + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + + Fantom.runTask(() => { + observer1 = new ResizeObserver(() => {}); + observer2 = new ResizeObserver(() => {}); + + observer1.observe(node); + observer2.observe(node); + + observer1.unobserve(node); + + // The second call shouldn't log errors (that would make the test fail). + observer2.unobserve(node); + }); + }); + }); + + describe('disconnect()', () => { + it('should do nothing if no targets are observed (not fail)', () => { + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.disconnect(); + }); + + expect(callback).not.toHaveBeenCalled(); + }); + + it('should stop observing all observed targets', () => { + const node1Ref = createRef(); + const node2Ref = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + <> + + + , + ); + }); + + const node1 = ensureReactNativeElement(node1Ref.current); + const node2 = ensureReactNativeElement(node2Ref.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node1); + observer.observe(node2); + }); + expect(callback).toHaveBeenCalledTimes(1); + + Fantom.runTask(() => { + observer.disconnect(); + }); + + Fantom.runTask(() => { + root.render( + <> + + + , + ); + }); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('should not dispatch pending entries when disconnecting', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + + // At the end of the current tick, we schedule delivery of the initial + // observation for the target. + observer.observe(node); + + // This is executed in the next tick, before the resize observer + // callback is called. + Fantom.scheduleTask(() => { + expect(callback).not.toHaveBeenCalled(); + + observer.disconnect(); + + expect(callback).toHaveBeenCalledTimes(0); + }); + }); + }); + + it('should correctly unobserve targets that are disconnected after observing', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + + Fantom.runTask(() => { + observer = new ResizeObserver(() => {}); + observer.observe(node); + }); + + Fantom.runTask(() => { + root.render(<>); + }); + expect(node.isConnected).toBe(false); + + expect(() => { + observer.disconnect(); + }).not.toThrow(); + }); + }); + + describe('ResizeObserverEntry', () => { + it('should freeze size arrays and keep stable getter identities', () => { + const nodeRef = createRef(); + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const node = ensureReactNativeElement(nodeRef.current); + const callback = jest.fn(); + + Fantom.runTask(() => { + observer = new ResizeObserver(callback); + observer.observe(node); + }); + + const [entries] = callback.mock.lastCall; + const entry = entries[0]; + + expect(Object.isFrozen(entry.contentBoxSize)).toBe(true); + expect(Object.isFrozen(entry.borderBoxSize)).toBe(true); + expect(Object.isFrozen(entry.devicePixelContentBoxSize)).toBe(true); + expect(entry.contentBoxSize).toBe(entry.contentBoxSize); + expect(entry.borderBoxSize).toBe(entry.borderBoxSize); + expect(entry.devicePixelContentBoxSize).toBe( + entry.devicePixelContentBoxSize, + ); + expect(entry.contentRect).toBe(entry.contentRect); + }); + }); + + describe('ResizeObserverEntry global constructor', () => { + it('throws when called', () => { + expect( + () => + // The public stub throws regardless of arguments; the real class + // requires two so Flow needs a suppression here. + // $FlowExpectedError[incompatible-type] + new ResizeObserverEntry(), + ).toThrow( + "Failed to construct 'ResizeObserverEntry': Illegal constructor", + ); + }); + }); + + describe('ResizeObserverSize global constructor', () => { + it('throws when called', () => { + expect( + () => + // The public stub throws regardless of arguments; the real class + // requires two so Flow needs a suppression here. + // $FlowExpectedError[incompatible-type] + new ResizeObserverSize(), + ).toThrow( + "Failed to construct 'ResizeObserverSize': Illegal constructor", + ); + }); + }); +}); diff --git a/private/react-native-fantom/tester/CMakeLists.txt b/private/react-native-fantom/tester/CMakeLists.txt index 590cd8261b9a..13e40b2b7fd2 100644 --- a/private/react-native-fantom/tester/CMakeLists.txt +++ b/private/react-native-fantom/tester/CMakeLists.txt @@ -71,6 +71,7 @@ add_react_common_subdir(react/nativemodule/idlecallbacks) add_react_common_subdir(react/nativemodule/intersectionobserver) add_react_common_subdir(react/nativemodule/microtasks) add_react_common_subdir(react/nativemodule/mutationobserver) +add_react_common_subdir(react/nativemodule/resizeobserver) add_react_common_subdir(react/nativemodule/viewtransition) add_react_common_subdir(react/nativemodule/webperformance) add_react_common_subdir(react/performance/cdpmetrics) @@ -101,6 +102,7 @@ add_react_common_subdir(react/renderer/mounting) add_react_common_subdir(react/renderer/observers/events) add_react_common_subdir(react/renderer/observers/intersection) add_react_common_subdir(react/renderer/observers/mutation) +add_react_common_subdir(react/renderer/observers/resize) add_react_common_subdir(react/renderer/runtimescheduler) add_react_common_subdir(react/renderer/scheduler) add_react_common_subdir(react/renderer/telemetry) @@ -189,6 +191,7 @@ target_link_libraries(fantom_tester react_nativemodule_intersectionobserver react_nativemodule_microtasks react_nativemodule_mutationobserver + react_nativemodule_resizeobserver react_nativemodule_viewtransition react_nativemodule_webperformance react_performance_cdpmetrics @@ -208,6 +211,7 @@ target_link_libraries(fantom_tester react_renderer_observers_events react_renderer_observers_intersection react_renderer_observers_mutation + react_renderer_observers_resize react_renderer_runtimescheduler react_renderer_scheduler react_renderer_telemetry From 2bde3647b8cc1a70ec7d041e55216488fcd5cf42 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 27 Jul 2026 14:33:05 +0200 Subject: [PATCH 23/39] fix: expose public wrapper --- .../src/private/setup/setUpResizeObserver.js | 4 +- .../resizeobserver/ResizeObserverSize.js | 37 ++++++++++++++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/react-native/src/private/setup/setUpResizeObserver.js b/packages/react-native/src/private/setup/setUpResizeObserver.js index 5f73a73b6947..17086f2ca8e2 100644 --- a/packages/react-native/src/private/setup/setUpResizeObserver.js +++ b/packages/react-native/src/private/setup/setUpResizeObserver.js @@ -33,6 +33,8 @@ export default function setUpResizeObserver() { polyfillGlobal( 'ResizeObserverSize', - () => require('../webapis/resizeobserver/ResizeObserverSize').default, + () => + require('../webapis/resizeobserver/ResizeObserverSize') + .ResizeObserverSize_public, ); } diff --git a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverSize.js b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverSize.js index 3fb961f4e3b9..105c6bd594a4 100644 --- a/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverSize.js +++ b/packages/react-native/src/private/webapis/resizeobserver/ResizeObserverSize.js @@ -17,8 +17,8 @@ import {setPlatformObject} from '../webidl/PlatformObjects'; * interface of the Resize Observer API is used to store the block and inline * sizes of a box as separate properties. * - * It is returned by the `contentBoxSize` and `borderBoxSize` properties of - * `ResizeObserverEntry`. + * It is returned by the `borderBoxSize`, `contentBoxSize` and + * `devicePixelContentBoxSize` properties of `ResizeObserverEntry`. */ export default class ResizeObserverSize { _inlineSize: number; @@ -30,20 +30,22 @@ export default class ResizeObserverSize { } /** - * The length of the observed element's border box, in the inline - * dimension. For boxes with a horizontal writing-mode, this is the vertical - * dimension, or height; if the writing-mode is vertical, this is the - * horizontal dimension, or width. + * The length of the observed box in the inline dimension. For boxes with a + * horizontal writing-mode, this is the horizontal dimension, or width; if + * the writing-mode is vertical, this is the vertical dimension, or height. + * + * React Native assumes a horizontal writing-mode, so this is the width. */ get inlineSize(): number { return this._inlineSize; } /** - * The length of the observed element's border box, in the block dimension. - * For boxes with a horizontal writing-mode, this is the vertical - * dimension, or height; if the writing-mode is vertical, this is the - * horizontal dimension, or width. + * The length of the observed box in the block dimension. For boxes with a + * horizontal writing-mode, this is the vertical dimension, or height; if the + * writing-mode is vertical, this is the horizontal dimension, or width. + * + * React Native assumes a horizontal writing-mode, so this is the height. */ get blockSize(): number { return this._blockSize; @@ -58,3 +60,18 @@ export function createResizeObserverSize( ): ResizeObserverSize { return new ResizeObserverSize(inlineSize, blockSize); } + +// `ResizeObserverSize` is not constructible from user code on the Web (its IDL +// declares no constructor). We expose this wrapper as the global instead of the +// class above, which we still use internally to build entries. +export const ResizeObserverSize_public: typeof ResizeObserverSize = + /* eslint-disable no-shadow */ + // $FlowExpectedError[incompatible-type] + function ResizeObserverSize() { + throw new TypeError( + "Failed to construct 'ResizeObserverSize': Illegal constructor", + ); + }; + +// $FlowExpectedError[prop-missing] +ResizeObserverSize_public.prototype = ResizeObserverSize.prototype; From 885847debafaf08be7c3ca72e5d0066208cbca19 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 27 Jul 2026 14:33:49 +0200 Subject: [PATCH 24/39] feat: update bom file --- packages/react-native/flow/bom.js.flow | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/react-native/flow/bom.js.flow b/packages/react-native/flow/bom.js.flow index a7045edfb507..62703357c833 100644 --- a/packages/react-native/flow/bom.js.flow +++ b/packages/react-native/flow/bom.js.flow @@ -418,6 +418,38 @@ declare class IntersectionObserver { unobserve(target: Element): void; } +declare type ResizeObserverSize = { + readonly blockSize: number, + readonly inlineSize: number, + ... +}; + +declare type ResizeObserverEntry = { + readonly borderBoxSize: ReadonlyArray, + readonly contentBoxSize: ReadonlyArray, + readonly devicePixelContentBoxSize: ReadonlyArray, + readonly contentRect: DOMRectReadOnly, + readonly target: Element, + ... +}; + +declare type ResizeObserverCallback = ( + entries: Array, + observer: ResizeObserver, +) => unknown; + +declare type ResizeObserverOptions = { + box?: 'content-box' | 'border-box' | 'device-pixel-content-box', + ... +}; + +declare class ResizeObserver { + constructor(callback: ResizeObserverCallback): void; + disconnect(): void; + observe(target: Element, options?: ResizeObserverOptions): void; + unobserve(target: Element): void; +} + declare class CloseEvent extends Event { code: number; reason: string; From a6987377ae77d1b0ab73ef02c82d9c564d46aea4 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Tue, 28 Jul 2026 11:25:26 +0200 Subject: [PATCH 25/39] feat: add docs --- .../resizeobserver/__docs__/README.md | 13 ++++ .../observers/resize/__docs__/README.md | 13 ++++ .../webapis/resizeobserver/__docs__/README.md | 57 ++++++++++++++++++ .../__docs__/architecture.excalidraw.png | Bin 0 -> 139322 bytes 4 files changed, 83 insertions(+) create mode 100644 packages/react-native/ReactCommon/react/nativemodule/resizeobserver/__docs__/README.md create mode 100644 packages/react-native/ReactCommon/react/renderer/observers/resize/__docs__/README.md create mode 100644 packages/react-native/src/private/webapis/resizeobserver/__docs__/README.md create mode 100644 packages/react-native/src/private/webapis/resizeobserver/__docs__/architecture.excalidraw.png diff --git a/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/__docs__/README.md b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/__docs__/README.md new file mode 100644 index 000000000000..dcd11199be2b --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/resizeobserver/__docs__/README.md @@ -0,0 +1,13 @@ +# ResizeObserver + +[🏠 Home](../../../../../../../__docs__/README.md) + +This directory contains the native module used by the +[ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) +in React Native. + +## 🔗 Relationship with other systems + +### Part of + +- [ResizeObserver API](../../../../../src/private/webapis/resizeobserver/__docs__/README.md) diff --git a/packages/react-native/ReactCommon/react/renderer/observers/resize/__docs__/README.md b/packages/react-native/ReactCommon/react/renderer/observers/resize/__docs__/README.md new file mode 100644 index 000000000000..eb9332910a34 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/observers/resize/__docs__/README.md @@ -0,0 +1,13 @@ +# ResizeObserver + +[🏠 Home](../../../../../../../../__docs__/README.md) + +This directory contains the C++ implementation of the +[ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) +in React Native. + +## 🔗 Relationship with other systems + +### Part of + +- [ResizeObserver API](../../../../../../src/private/webapis/resizeobserver/__docs__/README.md) diff --git a/packages/react-native/src/private/webapis/resizeobserver/__docs__/README.md b/packages/react-native/src/private/webapis/resizeobserver/__docs__/README.md new file mode 100644 index 000000000000..a7b8640358c2 --- /dev/null +++ b/packages/react-native/src/private/webapis/resizeobserver/__docs__/README.md @@ -0,0 +1,57 @@ +# ResizeObserver + +[🏠 Home](../../../../../../../__docs__/README.md) + +This directory contains the React Native implementation of the +[ResizeObserver API](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver). + +## 🚀 Usage + +`ResizeObserver` is meant to be used from JavaScript, exposed as a global +class. + +## 📐 Design + +This is the high-level design of the ResizeObserver API: + +![ResizeObserver architecture design](./architecture.excalidraw.png) + +The global `ResizeObserver` class is defined in JavaScript and it does its +setup using a native module. + +In native, it relies on commit hooks from `UIManager` to learn which nodes +had layout updates after each commit (`shadowTreeDidCommit` with the affected +layout nodes). The commit hook records which observed targets may have +resized. A dedicated step in the Event Loop (as specified in the Web spec, +via `RuntimeSchedulerResizeObserverDelegate`) then runs resize observations +against the latest committed tree and notifies JavaScript when there are +pending entries. JavaScript pulls those entries via `takeRecords` and +dispatches them to the right observers. + +Unlike layout events (`onLayout`), `ResizeObserver` lets callers choose which +box to observe (`content-box`, `border-box`, or `device-pixel-content-box`) and +delivers sizes for those boxes in the notification. It also integrates with +the Event Loop so resize-driven updates can be processed in the same UI update +as the resize itself. + +## 🔗 Relationship with other systems + +### Part of this + +- [NativeResizeObserver C++ TurboModule](../../../../../ReactCommon/react/nativemodule/resizeobserver/__docs__/README.md). +- [C++ implementation](../../../../../ReactCommon/react/renderer/observers/resize/__docs__/README.md). + +### Used by this + +- This relies on `ShadowTree` commit hooks provided by `UIManager`, including + the list of nodes whose layout changed after each commit. +- It uses the C++ TurboModule infra for communication between JavaScript and + native. +- It uses the + [`Event Loop`](../../../../../ReactCommon/react/renderer/runtimescheduler/__docs__/README.md) + to run resize observations as a dedicated step. + +### Uses this + +- This is an API meant to be used by end users. It is not used directly by any + other parts of the platform. diff --git a/packages/react-native/src/private/webapis/resizeobserver/__docs__/architecture.excalidraw.png b/packages/react-native/src/private/webapis/resizeobserver/__docs__/architecture.excalidraw.png new file mode 100644 index 0000000000000000000000000000000000000000..bb7c301778f154f3527302fb91a17eaf79c5edc8 GIT binary patch literal 139322 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�=jW(hDu-t03kbRhH&@# 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 Date: Tue, 28 Jul 2026 11:26:04 +0200 Subject: [PATCH 26/39] feat: rn-tester showcase page --- .../ResizeObserver/ResizeObserverIndex.js | 266 ++++++++++++++++++ .../js/utils/RNTesterList.android.js | 11 + .../rn-tester/js/utils/RNTesterList.ios.js | 11 + 3 files changed, 288 insertions(+) create mode 100644 packages/rn-tester/js/examples/ResizeObserver/ResizeObserverIndex.js diff --git a/packages/rn-tester/js/examples/ResizeObserver/ResizeObserverIndex.js b/packages/rn-tester/js/examples/ResizeObserver/ResizeObserverIndex.js new file mode 100644 index 000000000000..943c94fd7a7e --- /dev/null +++ b/packages/rn-tester/js/examples/ResizeObserver/ResizeObserverIndex.js @@ -0,0 +1,266 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; + +import {RNTesterThemeContext} from '../../components/RNTesterTheme'; +import * as React from 'react'; +import { + type ElementRef, + useContext, + useLayoutEffect, + useRef, + useState, +} from 'react'; +import {Button, StyleSheet, Text, View} from 'react-native'; + +const FONT_MIN_WIDTH = 80; +const FONT_MAX_WIDTH = 320; +const STEP = 40; + +function computeFontSize(contentWidth: number): number { + return Math.max(12, Math.min(44, Math.round(contentWidth / 8))); +} + +function FontScalingExample(): React.Node { + const theme = useContext(RNTesterThemeContext); + const boxRef = useRef>(null); + const [width, setWidth] = useState(200); + const [fontSize, setFontSize] = useState(computeFontSize(200)); + const [contentWidth, setContentWidth] = useState(null); + + useLayoutEffect(() => { + const box = boxRef.current; + if (box == null) { + return; + } + const observer = new ResizeObserver(entries => { + for (const entry of entries) { + const w = entry.contentRect.width; + setContentWidth(w); + setFontSize(computeFontSize(w)); + } + }); + // $FlowFixMe[incompatible-type] + observer.observe(box); + return () => observer.disconnect(); + }, []); + + return ( + + +