Skip to content

Commit 862e8c7

Browse files
RSNarafacebook-github-bot
authored andcommitted
IntersectionObserver: Clean up legacy observe/unobserve methods
Summary: In this diff, we migrated IntersectionObserver to tokens: D74262804. So that we wouldn't have to store shadow nodes on the javascript side. Storing shadow nodes lead to a memory leak in the past. Changelog: [internal] Reviewed By: rubennorte Differential Revision: D78494075 fbshipit-source-id: 38923ca4b265de6ff81ea20e5649e0bd2e39afc9
1 parent 23c8787 commit 862e8c7

File tree

7 files changed

+50
-157
lines changed

7 files changed

+50
-157
lines changed

packages/react-native/ReactCommon/react/nativemodule/intersectionobserver/NativeIntersectionObserver.cpp

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,6 @@ NativeIntersectionObserver::NativeIntersectionObserver(
4949
std::shared_ptr<CallInvoker> jsInvoker)
5050
: NativeIntersectionObserverCxxSpec(std::move(jsInvoker)) {}
5151

52-
void NativeIntersectionObserver::observe(
53-
jsi::Runtime& runtime,
54-
NativeIntersectionObserverObserveOptions options) {
55-
observeV2(runtime, std::move(options));
56-
}
57-
58-
void NativeIntersectionObserver::unobserve(
59-
jsi::Runtime& runtime,
60-
IntersectionObserverObserverId intersectionObserverId,
61-
std::shared_ptr<const ShadowNode> targetShadowNode) {
62-
auto token =
63-
tokenFromShadowNodeFamily(runtime, targetShadowNode->getFamilyShared());
64-
unobserveV2(runtime, intersectionObserverId, std::move(token));
65-
}
66-
6752
jsi::Object NativeIntersectionObserver::observeV2(
6853
jsi::Runtime& runtime,
6954
NativeIntersectionObserverObserveOptions options) {

packages/react-native/ReactCommon/react/nativemodule/intersectionobserver/NativeIntersectionObserver.h

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,6 @@ class NativeIntersectionObserver
6464
public:
6565
NativeIntersectionObserver(std::shared_ptr<CallInvoker> jsInvoker);
6666

67-
// TODO(T223605846): Remove legacy observe method
68-
[[deprecated("Please use observeV2")]]
69-
void observe(
70-
jsi::Runtime& runtime,
71-
NativeIntersectionObserverObserveOptions options);
72-
73-
// TODO(T223605846): Remove legacy unobserve method
74-
[[deprecated("Please use unobserveV2")]]
75-
void unobserve(
76-
jsi::Runtime& runtime,
77-
IntersectionObserverObserverId intersectionObserverId,
78-
std::shared_ptr<const ShadowNode> targetShadowNode);
79-
8067
jsi::Object observeV2(
8168
jsi::Runtime& runtime,
8269
NativeIntersectionObserverObserveOptions options);

packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -817,16 +817,6 @@ const definitions: FeatureFlagDefinitions = {
817817
},
818818
ossReleaseStage: 'none',
819819
},
820-
utilizeTokensInIntersectionObserver: {
821-
defaultValue: true,
822-
metadata: {
823-
dateAdded: '2025-05-06',
824-
description: 'Use tokens in IntersectionObserver vs ShadowNode.',
825-
expectedReleaseValue: true,
826-
purpose: 'experimentation',
827-
},
828-
ossReleaseStage: 'none',
829-
},
830820
},
831821
};
832822

packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<abb7c04a41689edfe7a46e371125ab30>>
7+
* @generated SignedSource<<a5b97c3df370225b7601f7375bb03ce0>>
88
* @flow strict
99
* @noformat
1010
*/
@@ -39,7 +39,6 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{
3939
shouldUseAnimatedObjectForTransform: Getter<boolean>,
4040
shouldUseRemoveClippedSubviewsAsDefaultOnIOS: Getter<boolean>,
4141
shouldUseSetNativePropsInFabric: Getter<boolean>,
42-
utilizeTokensInIntersectionObserver: Getter<boolean>,
4342
}>;
4443

4544
export type ReactNativeFeatureFlagsJsOnlyOverrides = OverridesFor<ReactNativeFeatureFlagsJsOnly>;
@@ -166,11 +165,6 @@ export const shouldUseRemoveClippedSubviewsAsDefaultOnIOS: Getter<boolean> = cre
166165
*/
167166
export const shouldUseSetNativePropsInFabric: Getter<boolean> = createJavaScriptFlagGetter('shouldUseSetNativePropsInFabric', true);
168167

169-
/**
170-
* Use tokens in IntersectionObserver vs ShadowNode.
171-
*/
172-
export const utilizeTokensInIntersectionObserver: Getter<boolean> = createJavaScriptFlagGetter('utilizeTokensInIntersectionObserver', true);
173-
174168
/**
175169
* Common flag for testing. Do NOT modify.
176170
*/

packages/react-native/src/private/webapis/intersectionobserver/__tests__/IntersectionObserver-itest.js

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @fantom_flags utilizeTokensInIntersectionObserver:*
87
* @flow strict-local
98
* @format
109
*/
@@ -20,7 +19,6 @@ import * as Fantom from '@react-native/fantom';
2019
import * as React from 'react';
2120
import {createRef, useState} from 'react';
2221
import {ScrollView, View} from 'react-native';
23-
import * as ReactNativeFeatureFlags from 'react-native/src/private/featureflags/ReactNativeFeatureFlags';
2422
import setUpIntersectionObserver from 'react-native/src/private/setup/setUpIntersectionObserver';
2523
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
2624
import DOMRectReadOnly from 'react-native/src/private/webapis/geometry/DOMRectReadOnly';
@@ -1494,44 +1492,42 @@ describe('IntersectionObserver', () => {
14941492
});
14951493
});
14961494

1497-
if (ReactNativeFeatureFlags.utilizeTokensInIntersectionObserver()) {
1498-
it('should not retain initial children of observed targets', () => {
1499-
const root = Fantom.createRoot();
1500-
observer = new IntersectionObserver(() => {});
1501-
1502-
const [getReferenceCount, ref] = createShadowNodeReferenceCountingRef();
1503-
1504-
const observeRef: React.RefSetter<
1505-
React.ElementRef<typeof View>,
1506-
> = instance => {
1507-
const element = ensureReactNativeElement(instance);
1508-
observer.observe(element);
1509-
return () => {
1510-
observer.unobserve(element);
1511-
};
1512-
};
1495+
it('should not retain initial children of observed targets', () => {
1496+
const root = Fantom.createRoot();
1497+
observer = new IntersectionObserver(() => {});
15131498

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

1518-
Fantom.runTask(() => {
1519-
root.render(
1520-
<Observe>
1521-
<View ref={ref} />
1522-
</Observe>,
1523-
);
1524-
});
1501+
const observeRef: React.RefSetter<
1502+
React.ElementRef<typeof View>,
1503+
> = instance => {
1504+
const element = ensureReactNativeElement(instance);
1505+
observer.observe(element);
1506+
return () => {
1507+
observer.unobserve(element);
1508+
};
1509+
};
15251510

1526-
expect(getReferenceCount()).toBeGreaterThan(0);
1511+
function Observe({children}: $ReadOnly<{children?: React.Node}>) {
1512+
return <View ref={observeRef}>{children}</View>;
1513+
}
15271514

1528-
Fantom.runTask(() => {
1529-
root.render(<Observe />);
1530-
});
1515+
Fantom.runTask(() => {
1516+
root.render(
1517+
<Observe>
1518+
<View ref={ref} />
1519+
</Observe>,
1520+
);
1521+
});
15311522

1532-
expect(getReferenceCount()).toBe(0);
1523+
expect(getReferenceCount()).toBeGreaterThan(0);
1524+
1525+
Fantom.runTask(() => {
1526+
root.render(<Observe />);
15331527
});
1534-
}
1528+
1529+
expect(getReferenceCount()).toBe(0);
1530+
});
15351531

15361532
it('should NOT report multiple entries when observing a target that exists and we modify it later in the same tick', () => {
15371533
const root = Fantom.createRoot({

packages/react-native/src/private/webapis/intersectionobserver/internals/IntersectionObserverManager.js

Lines changed: 19 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ import type {NativeIntersectionObserverToken} from '../specs/NativeIntersectionO
2727

2828
import * as Systrace from '../../../../../Libraries/Performance/Systrace';
2929
import warnOnce from '../../../../../Libraries/Utilities/warnOnce';
30-
import * as ReactNativeFeatureFlags from '../../../featureflags/ReactNativeFeatureFlags';
3130
import {
3231
getInstanceHandle,
3332
getNativeNodeReference,
3433
} from '../../dom/nodes/internals/NodeInternals';
3534
import {createIntersectionObserverEntry} from '../IntersectionObserverEntry';
3635
import NativeIntersectionObserver from '../specs/NativeIntersectionObserver';
36+
import nullthrows from 'nullthrows';
3737

3838
export type IntersectionObserverId = number;
3939

@@ -69,36 +69,11 @@ function setTargetForInstanceHandle(
6969
instanceHandleToTargetMap.set(key, target);
7070
}
7171

72-
// The mapping between ReactNativeElement and their corresponding shadow node
73-
// also needs to be kept here because React removes the link when unmounting.
74-
const targetToShadowNodeMap: WeakMap<
75-
ReactNativeElement,
76-
ReturnType<typeof getNativeNodeReference>,
77-
> = new WeakMap();
78-
7972
const targetToTokenMap: WeakMap<
8073
ReactNativeElement,
8174
NativeIntersectionObserverToken,
8275
> = new WeakMap();
8376

84-
let modernNativeIntersectionObserver =
85-
NativeIntersectionObserver == null
86-
? null
87-
: NativeIntersectionObserver.observeV2 == null ||
88-
NativeIntersectionObserver.unobserveV2 == null
89-
? null
90-
: {
91-
observe: NativeIntersectionObserver.observeV2,
92-
unobserve: NativeIntersectionObserver.unobserveV2,
93-
};
94-
95-
if (
96-
modernNativeIntersectionObserver &&
97-
!ReactNativeFeatureFlags.utilizeTokensInIntersectionObserver()
98-
) {
99-
modernNativeIntersectionObserver = null;
100-
}
101-
10277
/**
10378
* Registers the given intersection observer and returns a unique ID for it,
10479
* which is required to start observing targets.
@@ -189,34 +164,19 @@ export function observe({
189164
// access it even after the instance handle has been unmounted.
190165
setTargetForInstanceHandle(instanceHandle, target);
191166

192-
if (modernNativeIntersectionObserver == null) {
193-
// Same for the mapping between the target and its shadow node.
194-
targetToShadowNodeMap.set(target, targetNativeNodeReference);
195-
}
196-
197167
if (!isConnected) {
198168
NativeIntersectionObserver.connect(notifyIntersectionObservers);
199169
isConnected = true;
200170
}
201171

202-
if (modernNativeIntersectionObserver == null) {
203-
NativeIntersectionObserver.observe({
204-
intersectionObserverId,
205-
rootShadowNode: rootNativeNodeReference,
206-
targetShadowNode: targetNativeNodeReference,
207-
thresholds: registeredObserver.observer.thresholds,
208-
rootThresholds: registeredObserver.observer.rnRootThresholds,
209-
});
210-
} else {
211-
const token = modernNativeIntersectionObserver.observe({
212-
intersectionObserverId,
213-
rootShadowNode: rootNativeNodeReference,
214-
targetShadowNode: targetNativeNodeReference,
215-
thresholds: registeredObserver.observer.thresholds,
216-
rootThresholds: registeredObserver.observer.rnRootThresholds,
217-
});
218-
targetToTokenMap.set(target, token);
219-
}
172+
const token = nullthrows(NativeIntersectionObserver.observeV2)({
173+
intersectionObserverId,
174+
rootShadowNode: rootNativeNodeReference,
175+
targetShadowNode: targetNativeNodeReference,
176+
thresholds: registeredObserver.observer.thresholds,
177+
rootThresholds: registeredObserver.observer.rnRootThresholds,
178+
});
179+
targetToTokenMap.set(target, token);
220180

221181
return true;
222182
}
@@ -240,33 +200,18 @@ export function unobserve(
240200
return;
241201
}
242202

243-
if (modernNativeIntersectionObserver == null) {
244-
const targetNativeNodeReference = targetToShadowNodeMap.get(target);
245-
if (targetNativeNodeReference == null) {
246-
console.error(
247-
'IntersectionObserverManager: could not find registration data for target',
248-
);
249-
return;
250-
}
251-
252-
NativeIntersectionObserver.unobserve(
253-
intersectionObserverId,
254-
targetNativeNodeReference,
255-
);
256-
} else {
257-
const targetToken = targetToTokenMap.get(target);
258-
if (targetToken == null) {
259-
console.error(
260-
'IntersectionObserverManager: could not find registration data for target',
261-
);
262-
return;
263-
}
264-
265-
modernNativeIntersectionObserver.unobserve(
266-
intersectionObserverId,
267-
targetToken,
203+
const targetToken = targetToTokenMap.get(target);
204+
if (targetToken == null) {
205+
console.error(
206+
'IntersectionObserverManager: could not find registration data for target',
268207
);
208+
return;
269209
}
210+
211+
nullthrows(NativeIntersectionObserver.unobserveV2)(
212+
intersectionObserverId,
213+
targetToken,
214+
);
270215
}
271216

272217
/**

packages/react-native/src/private/webapis/intersectionobserver/specs/NativeIntersectionObserver.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ export type NativeIntersectionObserverObserveOptions = {
3434
export opaque type NativeIntersectionObserverToken = mixed;
3535

3636
export interface Spec extends TurboModule {
37-
// TODO(T223605846): Remove legacy observe method
38-
+observe: (options: NativeIntersectionObserverObserveOptions) => void;
39-
// TODO(T223605846): Remove legacy unobserve method
40-
+unobserve: (intersectionObserverId: number, targetShadowNode: mixed) => void;
4137
+observeV2?: (
4238
options: NativeIntersectionObserverObserveOptions,
4339
) => NativeIntersectionObserverToken;

0 commit comments

Comments
 (0)