IntersectionObserver: Migrate js infra to shadow node family (#51148)

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

Intersection observer should not be holding on to shadow nodes.

This diff migrates the javascript infra to instead use families.

Changelog: [Internal]

Reviewed By: yungsters

Differential Revision: D74262804

fbshipit-source-id: cc090be54f7312ce32b853ddf86567bb43e676b8
This commit is contained in:
Ramanpreet Nara
2025-05-08 20:52:13 -07:00
committed by Facebook GitHub Bot
parent a4be563935
commit e6516f2d7b
8 changed files with 167 additions and 37 deletions
@@ -21,6 +21,27 @@ NativeIntersectionObserverModuleProvider(
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<ShadowNodeFamily>(std::move(shadowNodeFamily)));
return obj;
}
ShadowNodeFamily::Shared shadowNodeFamilyFromToken(
jsi::Runtime& runtime,
jsi::Object token) {
return token.getNativeState<ShadowNodeFamily>(runtime);
}
} // namespace
NativeIntersectionObserver::NativeIntersectionObserver(
std::shared_ptr<CallInvoker> jsInvoker)
: NativeIntersectionObserverCxxSpec(std::move(jsInvoker)) {}
@@ -28,19 +49,7 @@ NativeIntersectionObserver::NativeIntersectionObserver(
void NativeIntersectionObserver::observe(
jsi::Runtime& runtime,
NativeIntersectionObserverObserveOptions options) {
auto intersectionObserverId = options.intersectionObserverId;
auto shadowNode =
shadowNodeFromValue(runtime, std::move(options.targetShadowNode));
auto thresholds = options.thresholds;
auto rootThresholds = options.rootThresholds;
auto& uiManager = getUIManagerFromRuntime(runtime);
intersectionObserverManager_.observe(
intersectionObserverId,
shadowNode->getFamilyShared(),
thresholds,
rootThresholds,
uiManager);
observeV2(runtime, std::move(options));
}
void NativeIntersectionObserver::unobserve(
@@ -48,8 +57,40 @@ void NativeIntersectionObserver::unobserve(
IntersectionObserverObserverId intersectionObserverId,
jsi::Object targetShadowNode) {
auto shadowNode = shadowNodeFromValue(runtime, std::move(targetShadowNode));
auto token =
tokenFromShadowNodeFamily(runtime, shadowNode->getFamilyShared());
unobserveV2(runtime, intersectionObserverId, std::move(token));
}
jsi::Object NativeIntersectionObserver::observeV2(
jsi::Runtime& runtime,
NativeIntersectionObserverObserveOptions options) {
auto intersectionObserverId = options.intersectionObserverId;
auto shadowNode =
shadowNodeFromValue(runtime, std::move(options.targetShadowNode));
auto shadowNodeFamily = shadowNode->getFamilyShared();
auto thresholds = options.thresholds;
auto rootThresholds = options.rootThresholds;
auto& uiManager = getUIManagerFromRuntime(runtime);
intersectionObserverManager_.observe(
intersectionObserverId,
shadowNodeFamily,
thresholds,
rootThresholds,
uiManager);
return tokenFromShadowNodeFamily(runtime, shadowNodeFamily);
}
void NativeIntersectionObserver::unobserveV2(
jsi::Runtime& runtime,
IntersectionObserverObserverId intersectionObserverId,
jsi::Object targetToken) {
auto shadowNodeFamily =
shadowNodeFamilyFromToken(runtime, std::move(targetToken));
intersectionObserverManager_.unobserve(
intersectionObserverId, shadowNode->getFamilyShared());
intersectionObserverId, shadowNodeFamily);
}
void NativeIntersectionObserver::connect(
@@ -61,15 +61,28 @@ class NativeIntersectionObserver
public:
NativeIntersectionObserver(std::shared_ptr<CallInvoker> jsInvoker);
// TODO(T223605846): Remove legacy observe method
[[deprecated("Please use observeV2")]]
void observe(
jsi::Runtime& runtime,
NativeIntersectionObserverObserveOptions options);
// TODO(T223605846): Remove legacy unobserve method
[[deprecated("Please use unobserveV2")]]
void unobserve(
jsi::Runtime& runtime,
IntersectionObserverObserverId intersectionObserverId,
jsi::Object targetShadowNode);
jsi::Object observeV2(
jsi::Runtime& runtime,
NativeIntersectionObserverObserveOptions options);
void unobserveV2(
jsi::Runtime& runtime,
IntersectionObserverObserverId intersectionObserverId,
jsi::Object targetToken);
void connect(
jsi::Runtime& runtime,
AsyncCallback<> notifyIntersectionObserversCallback);
@@ -39,7 +39,7 @@ struct ShadowNodeFamilyFragment {
* Represents all things that shadow nodes from the same family have in common.
* To be used inside `ShadowNode` class *only*.
*/
class ShadowNodeFamily final {
class ShadowNodeFamily final : public jsi::NativeState {
public:
using Shared = std::shared_ptr<const ShadowNodeFamily>;
using Weak = std::weak_ptr<const ShadowNodeFamily>;
@@ -663,6 +663,16 @@ const definitions: FeatureFlagDefinitions = {
},
ossReleaseStage: 'none',
},
utilizeTokensInIntersectionObserver: {
defaultValue: true,
metadata: {
dateAdded: '2025-05-06',
description: 'Use tokens in IntersectionObserver vs ShadowNode.',
expectedReleaseValue: true,
purpose: 'experimentation',
},
ossReleaseStage: 'none',
},
},
};
@@ -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<<6667fc8e4fdd2db0b54c908155b110cf>>
* @generated SignedSource<<17fa5e03fe52ed129cf731bba6e9869c>>
* @flow strict
*/
@@ -39,6 +39,7 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{
shouldUseAnimatedObjectForTransform: Getter<boolean>,
shouldUseRemoveClippedSubviewsAsDefaultOnIOS: Getter<boolean>,
shouldUseSetNativePropsInFabric: Getter<boolean>,
utilizeTokensInIntersectionObserver: Getter<boolean>,
}>;
export type ReactNativeFeatureFlagsJsOnlyOverrides = OverridesFor<ReactNativeFeatureFlagsJsOnly>;
@@ -155,6 +156,11 @@ export const shouldUseRemoveClippedSubviewsAsDefaultOnIOS: Getter<boolean> = cre
*/
export const shouldUseSetNativePropsInFabric: Getter<boolean> = createJavaScriptFlagGetter('shouldUseSetNativePropsInFabric', true);
/**
* Use tokens in IntersectionObserver vs ShadowNode.
*/
export const utilizeTokensInIntersectionObserver: Getter<boolean> = createJavaScriptFlagGetter('utilizeTokensInIntersectionObserver', true);
/**
* Common flag for testing. Do NOT modify.
*/
@@ -7,6 +7,7 @@
* @flow strict-local
* @format
* @oncall react_native
* @fantom_flags utilizeTokensInIntersectionObserver:true
*/
import 'react-native/Libraries/Core/InitializeCore';
@@ -844,8 +845,7 @@ describe('IntersectionObserver', () => {
});
});
// TODO (T223234714): Fix memory leak and enable this test.
it.skip('should not retain initial children of observed targets', () => {
it('should not retain initial children of observed targets', () => {
const root = Fantom.createRoot();
observer = new IntersectionObserver(() => {});
@@ -23,9 +23,11 @@ import type IntersectionObserver, {
IntersectionObserverCallback,
} from '../IntersectionObserver';
import type IntersectionObserverEntry from '../IntersectionObserverEntry';
import type {NativeIntersectionObserverToken} from '../specs/NativeIntersectionObserver';
import * as Systrace from '../../../../../Libraries/Performance/Systrace';
import warnOnce from '../../../../../Libraries/Utilities/warnOnce';
import * as ReactNativeFeatureFlags from '../../../featureflags/ReactNativeFeatureFlags';
import {
getInstanceHandle,
getNativeNodeReference,
@@ -74,6 +76,26 @@ const targetToShadowNodeMap: WeakMap<
ReturnType<typeof getNativeNodeReference>,
> = new WeakMap();
const targetToTokenMap: WeakMap<
ReactNativeElement,
NativeIntersectionObserverToken,
> = new WeakMap();
let modernNativeIntersectionObserver =
NativeIntersectionObserver == null
? null
: NativeIntersectionObserver.observeV2 == null ||
NativeIntersectionObserver.unobserveV2 == null
? null
: {
observe: NativeIntersectionObserver.observeV2,
unobserve: NativeIntersectionObserver.unobserveV2,
};
if (!ReactNativeFeatureFlags.utilizeTokensInIntersectionObserver()) {
modernNativeIntersectionObserver = null;
}
/**
* Registers the given intersection observer and returns a unique ID for it,
* which is required to start observing targets.
@@ -153,20 +175,32 @@ export function observe({
// access it even after the instance handle has been unmounted.
setTargetForInstanceHandle(instanceHandle, target);
// Same for the mapping between the target and its shadow node.
targetToShadowNodeMap.set(target, targetNativeNodeReference);
if (modernNativeIntersectionObserver == null) {
// Same for the mapping between the target and its shadow node.
targetToShadowNodeMap.set(target, targetNativeNodeReference);
}
if (!isConnected) {
NativeIntersectionObserver.connect(notifyIntersectionObservers);
isConnected = true;
}
NativeIntersectionObserver.observe({
intersectionObserverId,
targetShadowNode: targetNativeNodeReference,
thresholds: registeredObserver.observer.thresholds,
rootThresholds: registeredObserver.observer.rnRootThresholds,
});
if (modernNativeIntersectionObserver == null) {
NativeIntersectionObserver.observe({
intersectionObserverId,
targetShadowNode: targetNativeNodeReference,
thresholds: registeredObserver.observer.thresholds,
rootThresholds: registeredObserver.observer.rnRootThresholds,
});
} else {
const token = modernNativeIntersectionObserver.observe({
intersectionObserverId,
targetShadowNode: targetNativeNodeReference,
thresholds: registeredObserver.observer.thresholds,
rootThresholds: registeredObserver.observer.rnRootThresholds,
});
targetToTokenMap.set(target, token);
}
return true;
}
@@ -190,18 +224,33 @@ export function unobserve(
return;
}
const targetNativeNodeReference = targetToShadowNodeMap.get(target);
if (targetNativeNodeReference == null) {
console.error(
'IntersectionObserverManager: could not find registration data for target',
);
return;
}
if (modernNativeIntersectionObserver == null) {
const targetNativeNodeReference = targetToShadowNodeMap.get(target);
if (targetNativeNodeReference == null) {
console.error(
'IntersectionObserverManager: could not find registration data for target',
);
return;
}
NativeIntersectionObserver.unobserve(
intersectionObserverId,
targetNativeNodeReference,
);
NativeIntersectionObserver.unobserve(
intersectionObserverId,
targetNativeNodeReference,
);
} else {
const targetToken = targetToTokenMap.get(target);
if (targetToken == null) {
console.error(
'IntersectionObserverManager: could not find registration data for target',
);
return;
}
modernNativeIntersectionObserver.unobserve(
intersectionObserverId,
targetToken,
);
}
}
/**
@@ -30,9 +30,20 @@ export type NativeIntersectionObserverObserveOptions = {
rootThresholds?: ?$ReadOnlyArray<number>,
};
export type NativeIntersectionObserverToken = mixed;
export interface Spec extends TurboModule {
// TODO(T223605846): Remove legacy observe method
+observe: (options: NativeIntersectionObserverObserveOptions) => void;
// TODO(T223605846): Remove legacy unobserve method
+unobserve: (intersectionObserverId: number, targetShadowNode: mixed) => void;
+observeV2?: (
options: NativeIntersectionObserverObserveOptions,
) => NativeIntersectionObserverToken;
+unobserveV2?: (
intersectionObserverId: number,
token: NativeIntersectionObserverToken,
) => void;
+connect: (notifyIntersectionObserversCallback: () => void) => void;
+disconnect: () => void;
+takeRecords: () => $ReadOnlyArray<NativeIntersectionObserverEntry>;