Skip to content

IntersectionObserver: Clean up legacy observe/unobserve methods #52687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,6 @@ NativeIntersectionObserver::NativeIntersectionObserver(
std::shared_ptr<CallInvoker> jsInvoker)
: NativeIntersectionObserverCxxSpec(std::move(jsInvoker)) {}

void NativeIntersectionObserver::observe(
jsi::Runtime& runtime,
NativeIntersectionObserverObserveOptions options) {
observeV2(runtime, std::move(options));
}

void NativeIntersectionObserver::unobserve(
jsi::Runtime& runtime,
IntersectionObserverObserverId intersectionObserverId,
std::shared_ptr<const ShadowNode> targetShadowNode) {
auto token =
tokenFromShadowNodeFamily(runtime, targetShadowNode->getFamilyShared());
unobserveV2(runtime, intersectionObserverId, std::move(token));
}

jsi::Object NativeIntersectionObserver::observeV2(
jsi::Runtime& runtime,
NativeIntersectionObserverObserveOptions options) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,6 @@ 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,
std::shared_ptr<const ShadowNode> targetShadowNode);

jsi::Object observeV2(
jsi::Runtime& runtime,
NativeIntersectionObserverObserveOptions options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -856,16 +856,6 @@ 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',
},
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<<d30fc4e36ad600353c9b9098fd71430a>>
* @generated SignedSource<<a9d5d55d54a442f6b546c867bd832df2>>
* @flow strict
* @noformat
*/
Expand Down Expand Up @@ -43,7 +43,6 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{
shouldUseAnimatedObjectForTransform: Getter<boolean>,
shouldUseRemoveClippedSubviewsAsDefaultOnIOS: Getter<boolean>,
shouldUseSetNativePropsInFabric: Getter<boolean>,
utilizeTokensInIntersectionObserver: Getter<boolean>,
}>;

export type ReactNativeFeatureFlagsJsOnlyOverrides = OverridesFor<ReactNativeFeatureFlagsJsOnly>;
Expand Down Expand Up @@ -190,11 +189,6 @@ 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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @fantom_flags utilizeTokensInIntersectionObserver:*
* @flow strict-local
* @format
*/
Expand All @@ -20,7 +19,6 @@ import * as Fantom from '@react-native/fantom';
import * as React from 'react';
import {createRef, useState} from 'react';
import {ScrollView, View} from 'react-native';
import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags';
import setUpIntersectionObserver from 'react-native/src/private/setup/setUpIntersectionObserver';
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
import DOMRectReadOnly from 'react-native/src/private/webapis/geometry/DOMRectReadOnly';
Expand Down Expand Up @@ -1494,44 +1492,42 @@ describe('IntersectionObserver', () => {
});
});

if (ReactNativeFeatureFlags.utilizeTokensInIntersectionObserver()) {
it('should not retain initial children of observed targets', () => {
const root = Fantom.createRoot();
observer = new IntersectionObserver(() => {});

const [getReferenceCount, ref] = createShadowNodeReferenceCountingRef();

const observeRef: React.RefSetter<
React.ElementRef<typeof View>,
> = instance => {
const element = ensureReactNativeElement(instance);
observer.observe(element);
return () => {
observer.unobserve(element);
};
};
it('should not retain initial children of observed targets', () => {
const root = Fantom.createRoot();
observer = new IntersectionObserver(() => {});

function Observe({children}: $ReadOnly<{children?: React.Node}>) {
return <View ref={observeRef}>{children}</View>;
}
const [getReferenceCount, ref] = createShadowNodeReferenceCountingRef();

Fantom.runTask(() => {
root.render(
<Observe>
<View ref={ref} />
</Observe>,
);
});
const observeRef: React.RefSetter<
React.ElementRef<typeof View>,
> = instance => {
const element = ensureReactNativeElement(instance);
observer.observe(element);
return () => {
observer.unobserve(element);
};
};

expect(getReferenceCount()).toBeGreaterThan(0);
function Observe({children}: $ReadOnly<{children?: React.Node}>) {
return <View ref={observeRef}>{children}</View>;
}

Fantom.runTask(() => {
root.render(<Observe />);
});
Fantom.runTask(() => {
root.render(
<Observe>
<View ref={ref} />
</Observe>,
);
});

expect(getReferenceCount()).toBe(0);
expect(getReferenceCount()).toBeGreaterThan(0);

Fantom.runTask(() => {
root.render(<Observe />);
});
}

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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ import type {NativeIntersectionObserverToken} from '../specs/NativeIntersectionO

import * as Systrace from '../../../../../Libraries/Performance/Systrace';
import warnOnce from '../../../../../Libraries/Utilities/warnOnce';
import * as ReactNativeFeatureFlags from '../../../featureflags/ReactNativeFeatureFlags';
import {
getInstanceHandle,
getNativeNodeReference,
} from '../../dom/nodes/internals/NodeInternals';
import {createIntersectionObserverEntry} from '../IntersectionObserverEntry';
import NativeIntersectionObserver from '../specs/NativeIntersectionObserver';
import nullthrows from 'nullthrows';

export type IntersectionObserverId = number;

Expand Down Expand Up @@ -69,36 +69,11 @@ function setTargetForInstanceHandle(
instanceHandleToTargetMap.set(key, target);
}

// The mapping between ReactNativeElement and their corresponding shadow node
// also needs to be kept here because React removes the link when unmounting.
const targetToShadowNodeMap: WeakMap<
ReactNativeElement,
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 (
modernNativeIntersectionObserver &&
!ReactNativeFeatureFlags.utilizeTokensInIntersectionObserver()
) {
modernNativeIntersectionObserver = null;
}

/**
* Registers the given intersection observer and returns a unique ID for it,
* which is required to start observing targets.
Expand Down Expand Up @@ -189,34 +164,19 @@ export function observe({
// access it even after the instance handle has been unmounted.
setTargetForInstanceHandle(instanceHandle, target);

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;
}

if (modernNativeIntersectionObserver == null) {
NativeIntersectionObserver.observe({
intersectionObserverId,
rootShadowNode: rootNativeNodeReference,
targetShadowNode: targetNativeNodeReference,
thresholds: registeredObserver.observer.thresholds,
rootThresholds: registeredObserver.observer.rnRootThresholds,
});
} else {
const token = modernNativeIntersectionObserver.observe({
intersectionObserverId,
rootShadowNode: rootNativeNodeReference,
targetShadowNode: targetNativeNodeReference,
thresholds: registeredObserver.observer.thresholds,
rootThresholds: registeredObserver.observer.rnRootThresholds,
});
targetToTokenMap.set(target, token);
}
const token = nullthrows(NativeIntersectionObserver.observeV2)({
intersectionObserverId,
rootShadowNode: rootNativeNodeReference,
targetShadowNode: targetNativeNodeReference,
thresholds: registeredObserver.observer.thresholds,
rootThresholds: registeredObserver.observer.rnRootThresholds,
});
targetToTokenMap.set(target, token);

return true;
}
Expand All @@ -240,33 +200,18 @@ export function unobserve(
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,
);
} else {
const targetToken = targetToTokenMap.get(target);
if (targetToken == null) {
console.error(
'IntersectionObserverManager: could not find registration data for target',
);
return;
}

modernNativeIntersectionObserver.unobserve(
intersectionObserverId,
targetToken,
const targetToken = targetToTokenMap.get(target);
if (targetToken == null) {
console.error(
'IntersectionObserverManager: could not find registration data for target',
);
return;
}

nullthrows(NativeIntersectionObserver.unobserveV2)(
intersectionObserverId,
targetToken,
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ export type NativeIntersectionObserverObserveOptions = {
export opaque 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;
Expand Down
Loading