Integrate IntersectionObserver in Event Loop (#51452)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/51452

Changelog: [internal]

This implements a long planned refactor of `IntersectionObserver` to have a proper integration with the Event Loop, which unblocks `FragmentRef` in React Native.

The existing integration doesn't integrate with the Event Loop, so the intersection determination doesn't happen as a step in the event loop but in 2 different moments:
1. When you start observing a new target, we check if there are any pending transactions for that target, and otherwise determine the intersection immediately and queue the notifications.
2. Using mount hooks, when a transaction for a surfaceId is mounted, we check intersections for all the observers in that surface.

This has an important problem:
* If you attach a observer on a target before the target is attached to the tree (something that `FragmentRef` will start doing soon), we don't have a pending transaction so we determine intersections immediately. In that case, it's always "not visible" because it's not attached, and then we immediately trigger a transition when mounted in the same tick.

To fix this, we can implement a step in the Event Loop the same way that `IntersectionObserver` does on Web. We would still wait for pending transactions to be mounted to determine intersection timing (the same way we do now, but now checking at the end of the current Event Loop tick), but the dispatch of initial notifications for target that won't change is done at the end of the tick instead of synchronously.

It also refactors the list of active observers as a list of shared pointers to `IntersectionObserver` objects, instead of as list of `IntersectionObserver` objects directly. This is necessary to build the list of pending observers (with stable references) while making sure the ownership stays in the list of active observers.

Reviewed By: javache

Differential Revision: D74883214

fbshipit-source-id: 24accf1dba48d13b5773950a5cbf9ea38f4a1745
This commit is contained in:
Rubén Norte
2025-05-20 05:40:00 -07:00
committed by Facebook GitHub Bot
parent 50ce8c77a7
commit 3aeba22dda
35 changed files with 481 additions and 101 deletions
@@ -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<<e9a109fd77667dd0cb945ef6ef9737f2>>
* @generated SignedSource<<43590bc324e803ccf299e2a6ae6c5304>>
*/
/**
@@ -144,6 +144,12 @@ public object ReactNativeFeatureFlags {
@JvmStatic
public fun enableIOSViewClipToPaddingBox(): Boolean = accessor.enableIOSViewClipToPaddingBox()
/**
* Integrates IntersectionObserver in the Event Loop in the new architecture, to dispatch the initial notifications for observations in the "Update the rendering" step.
*/
@JvmStatic
public fun enableIntersectionObserverEventLoopIntegration(): Boolean = accessor.enableIntersectionObserverEventLoopIntegration()
/**
* When enabled, LayoutAnimations API will animate state changes on Android.
*/
@@ -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<<1a5cd689229d4e0c31070123045e84da>>
* @generated SignedSource<<5609be38b50c62ff5097b3571940f400>>
*/
/**
@@ -39,6 +39,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
private var enableFixForParentTagDuringReparentingCache: Boolean? = null
private var enableFontScaleChangesUpdatingLayoutCache: Boolean? = null
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
private var enableIntersectionObserverEventLoopIntegrationCache: Boolean? = null
private var enableLayoutAnimationsOnAndroidCache: Boolean? = null
private var enableLayoutAnimationsOnIOSCache: Boolean? = null
private var enableMainQueueModulesOnIOSCache: Boolean? = null
@@ -240,6 +241,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
return cached
}
override fun enableIntersectionObserverEventLoopIntegration(): Boolean {
var cached = enableIntersectionObserverEventLoopIntegrationCache
if (cached == null) {
cached = ReactNativeFeatureFlagsCxxInterop.enableIntersectionObserverEventLoopIntegration()
enableIntersectionObserverEventLoopIntegrationCache = cached
}
return cached
}
override fun enableLayoutAnimationsOnAndroid(): Boolean {
var cached = enableLayoutAnimationsOnAndroidCache
if (cached == null) {
@@ -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<<c39d0c1834797b3309b4fc5ce814280c>>
* @generated SignedSource<<15645e36bbca81dfcec07fc82f8f6264>>
*/
/**
@@ -66,6 +66,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
@DoNotStrip @JvmStatic public external fun enableIOSViewClipToPaddingBox(): Boolean
@DoNotStrip @JvmStatic public external fun enableIntersectionObserverEventLoopIntegration(): Boolean
@DoNotStrip @JvmStatic public external fun enableLayoutAnimationsOnAndroid(): Boolean
@DoNotStrip @JvmStatic public external fun enableLayoutAnimationsOnIOS(): Boolean
@@ -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<<7f357475254104729cc7910c14e1c1fb>>
* @generated SignedSource<<a8fa2410598d311ea5256aee7658ab3c>>
*/
/**
@@ -61,6 +61,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
override fun enableIOSViewClipToPaddingBox(): Boolean = false
override fun enableIntersectionObserverEventLoopIntegration(): Boolean = false
override fun enableLayoutAnimationsOnAndroid(): Boolean = false
override fun enableLayoutAnimationsOnIOS(): Boolean = true
@@ -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<<14cd1a58bd153dedda045a72c1494caa>>
* @generated SignedSource<<efc16fdb113176b932c0005118e7b48c>>
*/
/**
@@ -43,6 +43,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
private var enableFixForParentTagDuringReparentingCache: Boolean? = null
private var enableFontScaleChangesUpdatingLayoutCache: Boolean? = null
private var enableIOSViewClipToPaddingBoxCache: Boolean? = null
private var enableIntersectionObserverEventLoopIntegrationCache: Boolean? = null
private var enableLayoutAnimationsOnAndroidCache: Boolean? = null
private var enableLayoutAnimationsOnIOSCache: Boolean? = null
private var enableMainQueueModulesOnIOSCache: Boolean? = null
@@ -263,6 +264,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
return cached
}
override fun enableIntersectionObserverEventLoopIntegration(): Boolean {
var cached = enableIntersectionObserverEventLoopIntegrationCache
if (cached == null) {
cached = currentProvider.enableIntersectionObserverEventLoopIntegration()
accessedFeatureFlags.add("enableIntersectionObserverEventLoopIntegration")
enableIntersectionObserverEventLoopIntegrationCache = cached
}
return cached
}
override fun enableLayoutAnimationsOnAndroid(): Boolean {
var cached = enableLayoutAnimationsOnAndroidCache
if (cached == null) {
@@ -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<<593b1d64dc31038140032a6b0a439700>>
* @generated SignedSource<<3a61d9b66a7ed6fc9c76548aa72e3ed3>>
*/
/**
@@ -61,6 +61,8 @@ public interface ReactNativeFeatureFlagsProvider {
@DoNotStrip public fun enableIOSViewClipToPaddingBox(): Boolean
@DoNotStrip public fun enableIntersectionObserverEventLoopIntegration(): Boolean
@DoNotStrip public fun enableLayoutAnimationsOnAndroid(): Boolean
@DoNotStrip public fun enableLayoutAnimationsOnIOS(): Boolean
@@ -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<<d74dc8182a14fddbd7118149298515bd>>
* @generated SignedSource<<93037428475b39d3f2c208e1326b3e86>>
*/
/**
@@ -153,6 +153,12 @@ class ReactNativeFeatureFlagsJavaProvider
return method(javaProvider_);
}
bool enableIntersectionObserverEventLoopIntegration() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableIntersectionObserverEventLoopIntegration");
return method(javaProvider_);
}
bool enableLayoutAnimationsOnAndroid() override {
static const auto method =
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableLayoutAnimationsOnAndroid");
@@ -426,6 +432,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableIOSViewClipToPaddingBox(
return ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox();
}
bool JReactNativeFeatureFlagsCxxInterop::enableIntersectionObserverEventLoopIntegration(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableIntersectionObserverEventLoopIntegration();
}
bool JReactNativeFeatureFlagsCxxInterop::enableLayoutAnimationsOnAndroid(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
return ReactNativeFeatureFlags::enableLayoutAnimationsOnAndroid();
@@ -659,6 +670,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
makeNativeMethod(
"enableIOSViewClipToPaddingBox",
JReactNativeFeatureFlagsCxxInterop::enableIOSViewClipToPaddingBox),
makeNativeMethod(
"enableIntersectionObserverEventLoopIntegration",
JReactNativeFeatureFlagsCxxInterop::enableIntersectionObserverEventLoopIntegration),
makeNativeMethod(
"enableLayoutAnimationsOnAndroid",
JReactNativeFeatureFlagsCxxInterop::enableLayoutAnimationsOnAndroid),
@@ -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<<a5ab0022f6a01bd6e929d36d9d87db10>>
* @generated SignedSource<<f5365c0a71e4c7638cec79bebfab55c3>>
*/
/**
@@ -87,6 +87,9 @@ class JReactNativeFeatureFlagsCxxInterop
static bool enableIOSViewClipToPaddingBox(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
static bool enableIntersectionObserverEventLoopIntegration(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
static bool enableLayoutAnimationsOnAndroid(
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
@@ -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<<88fdbea2f97f628187164a47a9737da0>>
* @generated SignedSource<<e4946f3e524cd7571fc62c721d611d19>>
*/
/**
@@ -102,6 +102,10 @@ bool ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox() {
return getAccessor().enableIOSViewClipToPaddingBox();
}
bool ReactNativeFeatureFlags::enableIntersectionObserverEventLoopIntegration() {
return getAccessor().enableIntersectionObserverEventLoopIntegration();
}
bool ReactNativeFeatureFlags::enableLayoutAnimationsOnAndroid() {
return getAccessor().enableLayoutAnimationsOnAndroid();
}
@@ -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<<a4123bc6f44835c022a1a5238908674c>>
* @generated SignedSource<<83ef02f26eaf2d847efd7042ae6d1dfc>>
*/
/**
@@ -134,6 +134,11 @@ class ReactNativeFeatureFlags {
*/
RN_EXPORT static bool enableIOSViewClipToPaddingBox();
/**
* Integrates IntersectionObserver in the Event Loop in the new architecture, to dispatch the initial notifications for observations in the "Update the rendering" step.
*/
RN_EXPORT static bool enableIntersectionObserverEventLoopIntegration();
/**
* When enabled, LayoutAnimations API will animate state changes on Android.
*/
@@ -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<<0c768a58bd488ed1f3061cb4c13ef78d>>
* @generated SignedSource<<0d1e4f0581e3aa90e18dc70ccee8a50a>>
*/
/**
@@ -371,6 +371,24 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSViewClipToPaddingBox() {
return flagValue.value();
}
bool ReactNativeFeatureFlagsAccessor::enableIntersectionObserverEventLoopIntegration() {
auto flagValue = enableIntersectionObserverEventLoopIntegration_.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(19, "enableIntersectionObserverEventLoopIntegration");
flagValue = currentProvider_->enableIntersectionObserverEventLoopIntegration();
enableIntersectionObserverEventLoopIntegration_ = flagValue;
}
return flagValue.value();
}
bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnAndroid() {
auto flagValue = enableLayoutAnimationsOnAndroid_.load();
@@ -380,7 +398,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnAndroid() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(19, "enableLayoutAnimationsOnAndroid");
markFlagAsAccessed(20, "enableLayoutAnimationsOnAndroid");
flagValue = currentProvider_->enableLayoutAnimationsOnAndroid();
enableLayoutAnimationsOnAndroid_ = flagValue;
@@ -398,7 +416,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnIOS() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(20, "enableLayoutAnimationsOnIOS");
markFlagAsAccessed(21, "enableLayoutAnimationsOnIOS");
flagValue = currentProvider_->enableLayoutAnimationsOnIOS();
enableLayoutAnimationsOnIOS_ = flagValue;
@@ -416,7 +434,7 @@ bool ReactNativeFeatureFlagsAccessor::enableMainQueueModulesOnIOS() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(21, "enableMainQueueModulesOnIOS");
markFlagAsAccessed(22, "enableMainQueueModulesOnIOS");
flagValue = currentProvider_->enableMainQueueModulesOnIOS();
enableMainQueueModulesOnIOS_ = flagValue;
@@ -434,7 +452,7 @@ bool ReactNativeFeatureFlagsAccessor::enableNativeCSSParsing() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(22, "enableNativeCSSParsing");
markFlagAsAccessed(23, "enableNativeCSSParsing");
flagValue = currentProvider_->enableNativeCSSParsing();
enableNativeCSSParsing_ = flagValue;
@@ -452,7 +470,7 @@ bool ReactNativeFeatureFlagsAccessor::enableNetworkEventReporting() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(23, "enableNetworkEventReporting");
markFlagAsAccessed(24, "enableNetworkEventReporting");
flagValue = currentProvider_->enableNetworkEventReporting();
enableNetworkEventReporting_ = flagValue;
@@ -470,7 +488,7 @@ bool ReactNativeFeatureFlagsAccessor::enableNewBackgroundAndBorderDrawables() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(24, "enableNewBackgroundAndBorderDrawables");
markFlagAsAccessed(25, "enableNewBackgroundAndBorderDrawables");
flagValue = currentProvider_->enableNewBackgroundAndBorderDrawables();
enableNewBackgroundAndBorderDrawables_ = flagValue;
@@ -488,7 +506,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePreparedTextLayout() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(25, "enablePreparedTextLayout");
markFlagAsAccessed(26, "enablePreparedTextLayout");
flagValue = currentProvider_->enablePreparedTextLayout();
enablePreparedTextLayout_ = flagValue;
@@ -506,7 +524,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePropsUpdateReconciliationAndroid() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(26, "enablePropsUpdateReconciliationAndroid");
markFlagAsAccessed(27, "enablePropsUpdateReconciliationAndroid");
flagValue = currentProvider_->enablePropsUpdateReconciliationAndroid();
enablePropsUpdateReconciliationAndroid_ = flagValue;
@@ -524,7 +542,7 @@ bool ReactNativeFeatureFlagsAccessor::enableResourceTimingAPI() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(27, "enableResourceTimingAPI");
markFlagAsAccessed(28, "enableResourceTimingAPI");
flagValue = currentProvider_->enableResourceTimingAPI();
enableResourceTimingAPI_ = flagValue;
@@ -542,7 +560,7 @@ bool ReactNativeFeatureFlagsAccessor::enableSynchronousStateUpdates() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(28, "enableSynchronousStateUpdates");
markFlagAsAccessed(29, "enableSynchronousStateUpdates");
flagValue = currentProvider_->enableSynchronousStateUpdates();
enableSynchronousStateUpdates_ = flagValue;
@@ -560,7 +578,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(29, "enableViewCulling");
markFlagAsAccessed(30, "enableViewCulling");
flagValue = currentProvider_->enableViewCulling();
enableViewCulling_ = flagValue;
@@ -578,7 +596,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(30, "enableViewRecycling");
markFlagAsAccessed(31, "enableViewRecycling");
flagValue = currentProvider_->enableViewRecycling();
enableViewRecycling_ = flagValue;
@@ -596,7 +614,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(31, "enableViewRecyclingForText");
markFlagAsAccessed(32, "enableViewRecyclingForText");
flagValue = currentProvider_->enableViewRecyclingForText();
enableViewRecyclingForText_ = flagValue;
@@ -614,7 +632,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(32, "enableViewRecyclingForView");
markFlagAsAccessed(33, "enableViewRecyclingForView");
flagValue = currentProvider_->enableViewRecyclingForView();
enableViewRecyclingForView_ = flagValue;
@@ -632,7 +650,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(33, "fixMappingOfEventPrioritiesBetweenFabricAndReact");
markFlagAsAccessed(34, "fixMappingOfEventPrioritiesBetweenFabricAndReact");
flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact();
fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue;
@@ -650,7 +668,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(34, "fuseboxEnabledRelease");
markFlagAsAccessed(35, "fuseboxEnabledRelease");
flagValue = currentProvider_->fuseboxEnabledRelease();
fuseboxEnabledRelease_ = flagValue;
@@ -668,7 +686,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxNetworkInspectionEnabled() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(35, "fuseboxNetworkInspectionEnabled");
markFlagAsAccessed(36, "fuseboxNetworkInspectionEnabled");
flagValue = currentProvider_->fuseboxNetworkInspectionEnabled();
fuseboxNetworkInspectionEnabled_ = flagValue;
@@ -686,7 +704,7 @@ bool ReactNativeFeatureFlagsAccessor::incorporateMaxLinesDuringAndroidLayout() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(36, "incorporateMaxLinesDuringAndroidLayout");
markFlagAsAccessed(37, "incorporateMaxLinesDuringAndroidLayout");
flagValue = currentProvider_->incorporateMaxLinesDuringAndroidLayout();
incorporateMaxLinesDuringAndroidLayout_ = flagValue;
@@ -704,7 +722,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(37, "traceTurboModulePromiseRejectionsOnAndroid");
markFlagAsAccessed(38, "traceTurboModulePromiseRejectionsOnAndroid");
flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid();
traceTurboModulePromiseRejectionsOnAndroid_ = flagValue;
@@ -722,7 +740,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(38, "updateRuntimeShadowNodeReferencesOnCommit");
markFlagAsAccessed(39, "updateRuntimeShadowNodeReferencesOnCommit");
flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit();
updateRuntimeShadowNodeReferencesOnCommit_ = flagValue;
@@ -740,7 +758,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(39, "useAlwaysAvailableJSErrorHandling");
markFlagAsAccessed(40, "useAlwaysAvailableJSErrorHandling");
flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling();
useAlwaysAvailableJSErrorHandling_ = flagValue;
@@ -758,7 +776,7 @@ bool ReactNativeFeatureFlagsAccessor::useAndroidTextLayoutWidthDirectly() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(40, "useAndroidTextLayoutWidthDirectly");
markFlagAsAccessed(41, "useAndroidTextLayoutWidthDirectly");
flagValue = currentProvider_->useAndroidTextLayoutWidthDirectly();
useAndroidTextLayoutWidthDirectly_ = flagValue;
@@ -776,7 +794,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(41, "useFabricInterop");
markFlagAsAccessed(42, "useFabricInterop");
flagValue = currentProvider_->useFabricInterop();
useFabricInterop_ = flagValue;
@@ -794,7 +812,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(42, "useNativeViewConfigsInBridgelessMode");
markFlagAsAccessed(43, "useNativeViewConfigsInBridgelessMode");
flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode();
useNativeViewConfigsInBridgelessMode_ = flagValue;
@@ -812,7 +830,7 @@ bool ReactNativeFeatureFlagsAccessor::useOptimizedEventBatchingOnAndroid() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(43, "useOptimizedEventBatchingOnAndroid");
markFlagAsAccessed(44, "useOptimizedEventBatchingOnAndroid");
flagValue = currentProvider_->useOptimizedEventBatchingOnAndroid();
useOptimizedEventBatchingOnAndroid_ = flagValue;
@@ -830,7 +848,7 @@ bool ReactNativeFeatureFlagsAccessor::useRawPropsJsiValue() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(44, "useRawPropsJsiValue");
markFlagAsAccessed(45, "useRawPropsJsiValue");
flagValue = currentProvider_->useRawPropsJsiValue();
useRawPropsJsiValue_ = flagValue;
@@ -848,7 +866,7 @@ bool ReactNativeFeatureFlagsAccessor::useShadowNodeStateOnClone() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(45, "useShadowNodeStateOnClone");
markFlagAsAccessed(46, "useShadowNodeStateOnClone");
flagValue = currentProvider_->useShadowNodeStateOnClone();
useShadowNodeStateOnClone_ = flagValue;
@@ -866,7 +884,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(46, "useTurboModuleInterop");
markFlagAsAccessed(47, "useTurboModuleInterop");
flagValue = currentProvider_->useTurboModuleInterop();
useTurboModuleInterop_ = flagValue;
@@ -884,7 +902,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() {
// be accessing the provider multiple times but the end state of this
// instance and the returned flag value would be the same.
markFlagAsAccessed(47, "useTurboModules");
markFlagAsAccessed(48, "useTurboModules");
flagValue = currentProvider_->useTurboModules();
useTurboModules_ = flagValue;
@@ -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<<cf8c08ef77d045a6b0671139c7a003ad>>
* @generated SignedSource<<867dd37ee489203791b933b87562fc67>>
*/
/**
@@ -51,6 +51,7 @@ class ReactNativeFeatureFlagsAccessor {
bool enableFixForParentTagDuringReparenting();
bool enableFontScaleChangesUpdatingLayout();
bool enableIOSViewClipToPaddingBox();
bool enableIntersectionObserverEventLoopIntegration();
bool enableLayoutAnimationsOnAndroid();
bool enableLayoutAnimationsOnIOS();
bool enableMainQueueModulesOnIOS();
@@ -91,7 +92,7 @@ class ReactNativeFeatureFlagsAccessor {
std::unique_ptr<ReactNativeFeatureFlagsProvider> currentProvider_;
bool wasOverridden_;
std::array<std::atomic<const char*>, 48> accessedFeatureFlags_;
std::array<std::atomic<const char*>, 49> accessedFeatureFlags_;
std::atomic<std::optional<bool>> commonTestFlag_;
std::atomic<std::optional<bool>> animatedShouldSignalBatch_;
@@ -112,6 +113,7 @@ class ReactNativeFeatureFlagsAccessor {
std::atomic<std::optional<bool>> enableFixForParentTagDuringReparenting_;
std::atomic<std::optional<bool>> enableFontScaleChangesUpdatingLayout_;
std::atomic<std::optional<bool>> enableIOSViewClipToPaddingBox_;
std::atomic<std::optional<bool>> enableIntersectionObserverEventLoopIntegration_;
std::atomic<std::optional<bool>> enableLayoutAnimationsOnAndroid_;
std::atomic<std::optional<bool>> enableLayoutAnimationsOnIOS_;
std::atomic<std::optional<bool>> enableMainQueueModulesOnIOS_;
@@ -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<<829c85b56fbeaf01ece2d1dd84ecde8e>>
* @generated SignedSource<<c7ee2c12cbd79f431d5e61a8e5e8e860>>
*/
/**
@@ -103,6 +103,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider {
return false;
}
bool enableIntersectionObserverEventLoopIntegration() override {
return false;
}
bool enableLayoutAnimationsOnAndroid() override {
return false;
}
@@ -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<<d21af3f613aa450f25708c3da0cb6e97>>
* @generated SignedSource<<425dc272cba3224430f9948e6773b30f>>
*/
/**
@@ -216,6 +216,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef
return ReactNativeFeatureFlagsDefaults::enableIOSViewClipToPaddingBox();
}
bool enableIntersectionObserverEventLoopIntegration() override {
auto value = values_["enableIntersectionObserverEventLoopIntegration"];
if (!value.isNull()) {
return value.getBool();
}
return ReactNativeFeatureFlagsDefaults::enableIntersectionObserverEventLoopIntegration();
}
bool enableLayoutAnimationsOnAndroid() override {
auto value = values_["enableLayoutAnimationsOnAndroid"];
if (!value.isNull()) {
@@ -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<<25e38ea9e85a704f2f5ab6c7b8cb3062>>
* @generated SignedSource<<aae696b94ad35a3cdee45e80ad457a72>>
*/
/**
@@ -44,6 +44,7 @@ class ReactNativeFeatureFlagsProvider {
virtual bool enableFixForParentTagDuringReparenting() = 0;
virtual bool enableFontScaleChangesUpdatingLayout() = 0;
virtual bool enableIOSViewClipToPaddingBox() = 0;
virtual bool enableIntersectionObserverEventLoopIntegration() = 0;
virtual bool enableLayoutAnimationsOnAndroid() = 0;
virtual bool enableLayoutAnimationsOnIOS() = 0;
virtual bool enableMainQueueModulesOnIOS() = 0;
@@ -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<<3c85cdc2dc3d2368d09a5c0aff4d083a>>
* @generated SignedSource<<fadce2932514be743d6a1175daae39e2>>
*/
/**
@@ -139,6 +139,11 @@ bool NativeReactNativeFeatureFlags::enableIOSViewClipToPaddingBox(
return ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox();
}
bool NativeReactNativeFeatureFlags::enableIntersectionObserverEventLoopIntegration(
jsi::Runtime& /*runtime*/) {
return ReactNativeFeatureFlags::enableIntersectionObserverEventLoopIntegration();
}
bool NativeReactNativeFeatureFlags::enableLayoutAnimationsOnAndroid(
jsi::Runtime& /*runtime*/) {
return ReactNativeFeatureFlags::enableLayoutAnimationsOnAndroid();
@@ -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<<f7fbc7ab177cd84524a544cf3f94ae19>>
* @generated SignedSource<<33c4be38d881a6e09e4a58cac6eeca53>>
*/
/**
@@ -75,6 +75,8 @@ class NativeReactNativeFeatureFlags
bool enableIOSViewClipToPaddingBox(jsi::Runtime& runtime);
bool enableIntersectionObserverEventLoopIntegration(jsi::Runtime& runtime);
bool enableLayoutAnimationsOnAndroid(jsi::Runtime& runtime);
bool enableLayoutAnimationsOnIOS(jsi::Runtime& runtime);
@@ -7,6 +7,7 @@
#include "NativeIntersectionObserver.h"
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/runtimescheduler/RuntimeSchedulerBinding.h>
#include <react/renderer/uimanager/UIManagerBinding.h>
#include <react/renderer/uimanager/primitives.h>
@@ -98,12 +99,16 @@ void NativeIntersectionObserver::connect(
AsyncCallback<> notifyIntersectionObserversCallback) {
auto& uiManager = getUIManagerFromRuntime(runtime);
intersectionObserverManager_.connect(
uiManager, notifyIntersectionObserversCallback);
*RuntimeSchedulerBinding::getBinding(runtime)->getRuntimeScheduler(),
uiManager,
std::move(notifyIntersectionObserversCallback));
}
void NativeIntersectionObserver::disconnect(jsi::Runtime& runtime) {
auto& uiManager = getUIManagerFromRuntime(runtime);
intersectionObserverManager_.disconnect(uiManager);
intersectionObserverManager_.disconnect(
*RuntimeSchedulerBinding::getBinding(runtime)->getRuntimeScheduler(),
uiManager);
}
std::vector<NativeIntersectionObserverEntry>
@@ -123,7 +128,7 @@ NativeIntersectionObserver::takeRecords(jsi::Runtime& runtime) {
NativeIntersectionObserverEntry
NativeIntersectionObserver::convertToNativeModuleEntry(
IntersectionObserverEntry entry,
const IntersectionObserverEntry& entry,
jsi::Runtime& runtime) {
RectAsTuple targetRect = {
entry.targetRect.origin.x,
@@ -97,7 +97,7 @@ class NativeIntersectionObserver
static UIManager& getUIManagerFromRuntime(jsi::Runtime& runtime);
static NativeIntersectionObserverEntry convertToNativeModuleEntry(
IntersectionObserverEntry entry,
const IntersectionObserverEntry& entry,
jsi::Runtime& runtime);
};
@@ -57,10 +57,8 @@ class IntersectionObserver {
return intersectionObserverId_;
}
bool isTargetShadowNodeFamily(
const ShadowNodeFamily& shadowNodeFamily) const {
return std::addressof(*targetShadowNodeFamily_) ==
std::addressof(shadowNodeFamily);
ShadowNodeFamily::Shared getTargetShadowNodeFamily() const {
return targetShadowNodeFamily_;
}
std::vector<Float> getThresholds() const {
@@ -8,11 +8,33 @@
#include "IntersectionObserverManager.h"
#include <cxxreact/JSExecutor.h>
#include <cxxreact/TraceSection.h>
#include <react/debug/react_native_assert.h>
#include <utility>
#include "IntersectionObserver.h"
namespace facebook::react {
namespace {
RootShadowNode::Shared getRootShadowNode(
SurfaceId surfaceId,
const ShadowTreeRegistry& shadowTreeRegistry,
std::unordered_map<SurfaceId, RootShadowNode::Shared>& cache) {
auto it = cache.find(surfaceId);
if (it == cache.end()) {
RootShadowNode::Shared rootShadowNode = nullptr;
shadowTreeRegistry.visit(surfaceId, [&](const ShadowTree& shadowTree) {
rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode;
});
cache.insert({surfaceId, rootShadowNode});
return rootShadowNode;
} else {
return it->second;
}
}
} // namespace
IntersectionObserverManager::IntersectionObserverManager() = default;
void IntersectionObserverManager::observe(
@@ -27,50 +49,60 @@ void IntersectionObserverManager::observe(
// The actual observer lives in the array, so we need to create it there and
// then get a reference. Otherwise we only update its state in a copy.
IntersectionObserver* observer;
IntersectionObserver* observer = nullptr;
// Register observer
{
std::unique_lock lock(observersMutex_);
auto& observers = observersBySurfaceId_[surfaceId];
observers.emplace_back(IntersectionObserver{
observers.emplace_back(std::make_unique<IntersectionObserver>(
intersectionObserverId,
shadowNodeFamily,
std::move(thresholds),
std::move(rootThresholds)});
observer = &observers.back();
std::move(rootThresholds)));
if (ReactNativeFeatureFlags::
enableIntersectionObserverEventLoopIntegration()) {
observersPendingInitialization_.emplace_back(observers.back().get());
}
observer = observers.back().get();
}
// Notification of initial state.
// Ideally, we'd have well defined event loop step to notify observers
// (like on the Web) and we'd send the initial notification there, but as
// we don't have it we have to run this check once and manually dispatch.
auto& shadowTreeRegistry = uiManager.getShadowTreeRegistry();
std::shared_ptr<const MountingCoordinator> mountingCoordinator = nullptr;
RootShadowNode::Shared rootShadowNode = nullptr;
shadowTreeRegistry.visit(surfaceId, [&](const ShadowTree& shadowTree) {
mountingCoordinator = shadowTree.getMountingCoordinator();
rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode;
});
if (!ReactNativeFeatureFlags::
enableIntersectionObserverEventLoopIntegration()) {
// Notification of initial state.
// Ideally, we'd have well defined event loop step to notify observers
// (like on the Web) and we'd send the initial notification there, but as
// we don't have it we have to run this check once and manually dispatch.
auto& shadowTreeRegistry = uiManager.getShadowTreeRegistry();
std::shared_ptr<const MountingCoordinator> mountingCoordinator = nullptr;
RootShadowNode::Shared rootShadowNode = nullptr;
shadowTreeRegistry.visit(surfaceId, [&](const ShadowTree& shadowTree) {
mountingCoordinator = shadowTree.getMountingCoordinator();
rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode;
});
// If the surface doesn't exist for some reason, we skip initial notification.
if (!rootShadowNode) {
return;
}
// If the surface doesn't exist for some reason, we skip initial
// notification.
if (!rootShadowNode) {
return;
}
auto hasPendingTransactions = mountingCoordinator != nullptr &&
mountingCoordinator->hasPendingTransactions();
auto hasPendingTransactions = mountingCoordinator != nullptr &&
mountingCoordinator->hasPendingTransactions();
if (!hasPendingTransactions) {
auto entry = observer->updateIntersectionObservation(
*rootShadowNode, JSExecutor::performanceNow());
if (entry) {
{
std::unique_lock lock(pendingEntriesMutex_);
pendingEntries_.push_back(std::move(entry).value());
if (!hasPendingTransactions) {
auto entry = observer->updateIntersectionObservation(
*rootShadowNode, JSExecutor::performanceNow());
if (entry) {
{
std::unique_lock lock(pendingEntriesMutex_);
pendingEntries_.push_back(std::move(entry).value());
}
notifyObserversIfNecessary();
}
notifyObserversIfNecessary();
}
}
}
@@ -80,6 +112,23 @@ void IntersectionObserverManager::unobserve(
const ShadowNodeFamily::Shared& shadowNodeFamily) {
TraceSection s("IntersectionObserverManager::unobserve");
if (ReactNativeFeatureFlags::
enableIntersectionObserverEventLoopIntegration()) {
// This doesn't need to be protected by the mutex because it is only
// accessed and modified from the JS thread.
observersPendingInitialization_.erase(
std::remove_if(
observersPendingInitialization_.begin(),
observersPendingInitialization_.end(),
[intersectionObserverId, &shadowNodeFamily](const auto& observer) {
return (
observer->getIntersectionObserverId() ==
intersectionObserverId &&
observer->getTargetShadowNodeFamily() == shadowNodeFamily);
}),
observersPendingInitialization_.end());
}
{
std::unique_lock lock(observersMutex_);
@@ -97,9 +146,9 @@ void IntersectionObserverManager::unobserve(
observers.begin(),
observers.end(),
[intersectionObserverId, &shadowNodeFamily](const auto& observer) {
return observer.getIntersectionObserverId() ==
return observer->getIntersectionObserverId() ==
intersectionObserverId &&
observer.isTargetShadowNodeFamily(*shadowNodeFamily);
observer->getTargetShadowNodeFamily() == shadowNodeFamily;
}),
observers.end());
@@ -124,6 +173,7 @@ void IntersectionObserverManager::unobserve(
}
void IntersectionObserverManager::connect(
RuntimeScheduler& runtimeScheduler,
UIManager& uiManager,
std::function<void()> notifyIntersectionObserversCallback) {
TraceSection s("IntersectionObserverManager::connect");
@@ -135,11 +185,18 @@ void IntersectionObserverManager::connect(
return;
}
if (ReactNativeFeatureFlags::
enableIntersectionObserverEventLoopIntegration()) {
runtimeScheduler.setIntersectionObserverDelegate(this);
}
uiManager.registerMountHook(*this);
shadowTreeRegistry_ = &uiManager.getShadowTreeRegistry();
mountHookRegistered_ = true;
}
void IntersectionObserverManager::disconnect(UIManager& uiManager) {
void IntersectionObserverManager::disconnect(
RuntimeScheduler& runtimeScheduler,
UIManager& uiManager) {
TraceSection s("IntersectionObserverManager::disconnect");
// Fail-safe in case the caller doesn't guarantee consistency.
@@ -147,7 +204,12 @@ void IntersectionObserverManager::disconnect(UIManager& uiManager) {
return;
}
if (ReactNativeFeatureFlags::
enableIntersectionObserverEventLoopIntegration()) {
runtimeScheduler.setIntersectionObserverDelegate(nullptr);
}
uiManager.unregisterMountHook(*this);
shadowTreeRegistry_ = nullptr;
mountHookRegistered_ = false;
notifyIntersectionObserversCallback_ = nullptr;
}
@@ -163,6 +225,57 @@ IntersectionObserverManager::takeRecords() {
return entries;
}
#pragma mark - RuntimeSchedulerIntersectionObserverDelegate
void IntersectionObserverManager::updateIntersectionObservations(
const std::unordered_set<SurfaceId>&
surfaceIdsWithPendingRenderingUpdates) {
// On Web, this step invoked from the Event Loop computes the intersections
// and schedules the notifications.
// Doing exactly the same in React Native would exclude the time it takes
// to process these transactions and mount the operations in the host
// platform, which would provide inaccurate timings for measuring paint time.
// Instead, we use mount hooks to compute this.
// What we can use this step for is to dispatch initial notifications for
// observers that were just set up, but for which we do not have any pending
// transactions that would trigger the mount hooks.
// In those cases it is ok to dispatch the notifications now, because the
// current state is already accurate.
std::unordered_map<SurfaceId, RootShadowNode::Shared> rootShadowNodeCache;
for (auto observer : observersPendingInitialization_) {
auto surfaceId = observer->getTargetShadowNodeFamily()->getSurfaceId();
// If there are pending updates, we just wait for the mount hook.
if (surfaceIdsWithPendingRenderingUpdates.contains(surfaceId)) {
continue;
}
RootShadowNode::Shared rootShadowNode =
getRootShadowNode(surfaceId, *shadowTreeRegistry_, rootShadowNodeCache);
// If the surface doesn't exist for some reason, we skip initial
// notification.
if (!rootShadowNode) {
continue;
}
auto entry = observer->updateIntersectionObservation(
*rootShadowNode, JSExecutor::performanceNow());
if (entry) {
{
std::unique_lock lock(pendingEntriesMutex_);
pendingEntries_.push_back(std::move(entry).value());
}
notifyObserversIfNecessary();
}
}
observersPendingInitialization_.clear();
}
#pragma mark - UIManagerMountHook
void IntersectionObserverManager::shadowTreeDidMount(
@@ -202,9 +315,9 @@ void IntersectionObserverManager::updateIntersectionObservations(
std::optional<IntersectionObserverEntry> entry;
if (rootShadowNode != nullptr) {
entry = observer.updateIntersectionObservation(*rootShadowNode, time);
entry = observer->updateIntersectionObservation(*rootShadowNode, time);
} else {
entry = observer.updateIntersectionObservationForSurfaceUnmount(time);
entry = observer->updateIntersectionObservationForSurfaceUnmount(time);
}
if (entry) {
@@ -9,14 +9,19 @@
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/graphics/Float.h>
#include <react/renderer/runtimescheduler/RuntimeScheduler.h>
#include <react/renderer/runtimescheduler/RuntimeSchedulerIntersectionObserverDelegate.h>
#include <react/renderer/uimanager/UIManager.h>
#include <react/renderer/uimanager/UIManagerMountHook.h>
#include <memory>
#include <vector>
#include "IntersectionObserver.h"
namespace facebook::react {
class IntersectionObserverManager final : public UIManagerMountHook {
class IntersectionObserverManager final
: public UIManagerMountHook,
public RuntimeSchedulerIntersectionObserverDelegate {
public:
IntersectionObserverManager();
@@ -32,13 +37,20 @@ class IntersectionObserverManager final : public UIManagerMountHook {
const ShadowNodeFamily::Shared& shadowNode);
void connect(
RuntimeScheduler& runtimeScheduler,
UIManager& uiManager,
std::function<void()> notifyIntersectionObserversCallback);
void disconnect(UIManager& uiManager);
void disconnect(RuntimeScheduler& runtimeScheduler, UIManager& uiManager);
std::vector<IntersectionObserverEntry> takeRecords();
#pragma mark - RuntimeSchedulerIntersectionObserverDelegate
void updateIntersectionObservations(
const std::unordered_set<SurfaceId>&
surfaceIdsWithPendingRenderingUpdates) override;
#pragma mark - UIManagerMountHook
void shadowTreeDidMount(
@@ -48,10 +60,22 @@ class IntersectionObserverManager final : public UIManagerMountHook {
void shadowTreeDidUnmount(SurfaceId surfaceId, double time) noexcept override;
private:
mutable std::unordered_map<SurfaceId, std::vector<IntersectionObserver>>
mutable std::unordered_map<
SurfaceId,
std::vector<std::unique_ptr<IntersectionObserver>>>
observersBySurfaceId_;
mutable std::shared_mutex observersMutex_;
// This is defined as a list of pointers to keep the ownership of the
// observers in the map.
std::vector<IntersectionObserver*> observersPendingInitialization_;
// 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 `updateIntersectionObservations`.
const ShadowTreeRegistry* shadowTreeRegistry_{nullptr};
mutable std::function<void()> notifyIntersectionObserversCallback_;
mutable std::vector<IntersectionObserverEntry> pendingEntries_;
@@ -126,4 +126,11 @@ void RuntimeScheduler::setEventTimingDelegate(
return runtimeSchedulerImpl_->setEventTimingDelegate(eventTimingDelegate);
}
void RuntimeScheduler::setIntersectionObserverDelegate(
RuntimeSchedulerIntersectionObserverDelegate*
intersectionObserverDelegate) {
return runtimeSchedulerImpl_->setIntersectionObserverDelegate(
intersectionObserverDelegate);
}
} // namespace facebook::react
@@ -14,6 +14,7 @@
#include <react/renderer/runtimescheduler/Task.h>
#include <react/timing/primitives.h>
#include "RuntimeSchedulerEventTimingDelegate.h"
#include "RuntimeSchedulerIntersectionObserverDelegate.h"
namespace facebook::react {
@@ -60,6 +61,9 @@ class RuntimeSchedulerBase {
PerformanceEntryReporter* reporter) = 0;
virtual void setEventTimingDelegate(
RuntimeSchedulerEventTimingDelegate* eventTimingDelegate) = 0;
virtual void setIntersectionObserverDelegate(
RuntimeSchedulerIntersectionObserverDelegate*
intersectionObserverDelegate) = 0;
};
// This is a proxy for RuntimeScheduler implementation, which will be selected
@@ -172,6 +176,10 @@ class RuntimeScheduler final : RuntimeSchedulerBase {
void setEventTimingDelegate(
RuntimeSchedulerEventTimingDelegate* eventTimingDelegate) override;
void setIntersectionObserverDelegate(
RuntimeSchedulerIntersectionObserverDelegate*
intersectionObserverDelegate) override;
private:
// Actual implementation, stored as a unique pointer to simplify memory
// management.
@@ -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.
*/
#pragma once
#include <unordered_set>
namespace facebook::react {
using SurfaceId = int32_t;
class RuntimeSchedulerIntersectionObserverDelegate {
public:
virtual ~RuntimeSchedulerIntersectionObserverDelegate() = default;
virtual void updateIntersectionObservations(
const std::unordered_set<SurfaceId>&
surfaceIdsWithPendingRenderingUpdates) = 0;
};
} // namespace facebook::react
@@ -204,6 +204,12 @@ void RuntimeScheduler_Legacy::setEventTimingDelegate(
// No-op in the legacy scheduler
}
void RuntimeScheduler_Legacy::setIntersectionObserverDelegate(
RuntimeSchedulerIntersectionObserverDelegate*
/*intersectionObserverDelegate*/) {
// No-op in the legacy scheduler
}
#pragma mark - Private
void RuntimeScheduler_Legacy::scheduleWorkLoopIfNecessary() {
@@ -134,6 +134,10 @@ class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase {
void setEventTimingDelegate(
RuntimeSchedulerEventTimingDelegate* eventTimingDelegate) override;
void setIntersectionObserverDelegate(
RuntimeSchedulerIntersectionObserverDelegate*
intersectionObserverDelegate) override;
private:
std::priority_queue<
std::shared_ptr<Task>,
@@ -225,6 +225,12 @@ void RuntimeScheduler_Modern::setEventTimingDelegate(
eventTimingDelegate_ = eventTimingDelegate;
}
void RuntimeScheduler_Modern::setIntersectionObserverDelegate(
RuntimeSchedulerIntersectionObserverDelegate*
intersectionObserverDelegate) {
intersectionObserverDelegate_ = intersectionObserverDelegate;
}
#pragma mark - Private
void RuntimeScheduler_Modern::scheduleTask(std::shared_ptr<Task> task) {
@@ -347,11 +353,20 @@ void RuntimeScheduler_Modern::runEventLoopTick(
void RuntimeScheduler_Modern::updateRendering() {
TraceSection s("RuntimeScheduler::updateRendering");
// This is the integration of the Event Timing API in the Event Loop.
// See https://w3c.github.io/event-timing/#sec-modifications-HTML
if (eventTimingDelegate_ != nullptr) {
eventTimingDelegate_->dispatchPendingEventTimingEntries(
surfaceIdsWithPendingRenderingUpdates_);
}
// This is the integration of the Intersection Observer API in the Event Loop.
// See
if (intersectionObserverDelegate_ != nullptr) {
intersectionObserverDelegate_->updateIntersectionObservations(
surfaceIdsWithPendingRenderingUpdates_);
}
surfaceIdsWithPendingRenderingUpdates_.clear();
while (!pendingRenderingUpdates_.empty()) {
@@ -151,6 +151,10 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase {
void setEventTimingDelegate(
RuntimeSchedulerEventTimingDelegate* eventTimingDelegate) override;
void setIntersectionObserverDelegate(
RuntimeSchedulerIntersectionObserverDelegate*
intersectionObserverDelegate) override;
private:
std::atomic<uint_fast8_t> syncTaskRequests_{0};
@@ -229,6 +233,8 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase {
PerformanceEntryReporter* performanceEntryReporter_{nullptr};
RuntimeSchedulerEventTimingDelegate* eventTimingDelegate_{nullptr};
RuntimeSchedulerIntersectionObserverDelegate* intersectionObserverDelegate_{
nullptr};
RuntimeSchedulerTaskErrorHandler onTaskError_;
};
@@ -247,6 +247,17 @@ const definitions: FeatureFlagDefinitions = {
},
ossReleaseStage: 'none',
},
enableIntersectionObserverEventLoopIntegration: {
defaultValue: false,
metadata: {
dateAdded: '2025-04-16',
description:
'Integrates IntersectionObserver in the Event Loop in the new architecture, to dispatch the initial notifications for observations in the "Update the rendering" step.',
expectedReleaseValue: true,
purpose: 'experimentation',
},
ossReleaseStage: 'none',
},
enableLayoutAnimationsOnAndroid: {
defaultValue: false,
metadata: {
@@ -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<<29691b2062c240377834a01ced24c71c>>
* @generated SignedSource<<faa532fc9396b30f13cb55f59fbe7f8e>>
* @flow strict
* @noformat
*/
@@ -70,6 +70,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{
enableFixForParentTagDuringReparenting: Getter<boolean>,
enableFontScaleChangesUpdatingLayout: Getter<boolean>,
enableIOSViewClipToPaddingBox: Getter<boolean>,
enableIntersectionObserverEventLoopIntegration: Getter<boolean>,
enableLayoutAnimationsOnAndroid: Getter<boolean>,
enableLayoutAnimationsOnIOS: Getter<boolean>,
enableMainQueueModulesOnIOS: Getter<boolean>,
@@ -261,6 +262,10 @@ export const enableFontScaleChangesUpdatingLayout: Getter<boolean> = createNativ
* iOS Views will clip to their padding box vs border box
*/
export const enableIOSViewClipToPaddingBox: Getter<boolean> = createNativeFlagGetter('enableIOSViewClipToPaddingBox', false);
/**
* Integrates IntersectionObserver in the Event Loop in the new architecture, to dispatch the initial notifications for observations in the "Update the rendering" step.
*/
export const enableIntersectionObserverEventLoopIntegration: Getter<boolean> = createNativeFlagGetter('enableIntersectionObserverEventLoopIntegration', false);
/**
* When enabled, LayoutAnimations API will animate state changes on Android.
*/
@@ -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<<59ebee6019b99a8c6a29dfc1eb036194>>
* @generated SignedSource<<ddb43daeb855838f8d396dfa952e9933>>
* @flow strict
* @noformat
*/
@@ -44,6 +44,7 @@ export interface Spec extends TurboModule {
+enableFixForParentTagDuringReparenting?: () => boolean;
+enableFontScaleChangesUpdatingLayout?: () => boolean;
+enableIOSViewClipToPaddingBox?: () => boolean;
+enableIntersectionObserverEventLoopIntegration?: () => boolean;
+enableLayoutAnimationsOnAndroid?: () => boolean;
+enableLayoutAnimationsOnIOS?: () => boolean;
+enableMainQueueModulesOnIOS?: () => boolean;
@@ -27,10 +27,11 @@ surface. If the intersection triggers a state change, the notification is
dispatched to the JavaScript observers.
For the initial intersection notification (which is meant to report the state at
the time of observation), this system checks if there are pending transactions
for the given surface. If there are, we just wait for the transaction to be
mounted and use the mount hook to report the initial notification. If there
aren't, we dispatch the notification immediately.
the time of observation), we run a step in the Event Loop as specified in the
Web spec (via `RuntimeSchedulerIntersectionObserverDelegate`), where we check if
there are pending transactions for the given surface. If there are, we just wait
for the transaction to be mounted and use the mount hook to report the initial
notification. If there aren't, we dispatch the notification immediately.
## 🔗 Relationship with other systems
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 84 KiB

@@ -7,6 +7,7 @@
* @flow strict-local
* @format
* @fantom_flags utilizeTokensInIntersectionObserver:true
* @fantom_flags enableIntersectionObserverEventLoopIntegration:true
*/
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
@@ -18,7 +19,7 @@ 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 {createRef, useState} from 'react';
import {ScrollView, View} from 'react-native';
import setUpIntersectionObserver from 'react-native/src/private/setup/setUpIntersectionObserver';
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
@@ -882,6 +883,55 @@ describe('IntersectionObserver', () => {
expect(getReferenceCount()).toBe(0);
});
it('should NOT report multiple entries when observing a target that exists and we modify it later in the same tick', () => {
const root = Fantom.createRoot({
viewportWidth: 1000,
viewportHeight: 1000,
});
const nodeRef = createRef<HostInstance>();
function TestComponent() {
const [showView, setShowView] = useState(true);
return showView ? (
<View
onClick={() => {
observer.observe(ensureReactNativeElement(nodeRef.current));
setShowView(false);
}}
style={{width: 100, height: 100, backgroundColor: 'red'}}
ref={nodeRef}
/>
) : null;
}
Fantom.runTask(() => {
root.render(<TestComponent />);
});
const node = ensureReactNativeElement(nodeRef.current);
expect(node.isConnected).toBe(true);
const intersectionObserverCallback = jest.fn();
Fantom.runTask(() => {
observer = new IntersectionObserver(intersectionObserverCallback);
});
// We use a discrete event to make sure React processes the update in the
// same task.
Fantom.dispatchNativeEvent(node, 'click');
expect(node.isConnected).toBe(false);
expect(intersectionObserverCallback).toHaveBeenCalledTimes(1);
const [entries] = intersectionObserverCallback.mock.lastCall;
expect(entries.length).toBe(1);
expect(entries[0].isIntersecting).toBe(false);
});
describe('rootThreshold', () => {
it('should report partial intersecting initial state correctly', () => {
const nodeRef = createRef<HostInstance>();