diff --git a/packages/react-native/Libraries/IntersectionObserver/IntersectionObserverEntry.js b/packages/react-native/Libraries/IntersectionObserver/IntersectionObserverEntry.js index 315a53446c63e5..043d66742564a1 100644 --- a/packages/react-native/Libraries/IntersectionObserver/IntersectionObserverEntry.js +++ b/packages/react-native/Libraries/IntersectionObserver/IntersectionObserverEntry.js @@ -11,11 +11,9 @@ // flowlint unsafe-getters-setters:off import type ReactNativeElement from '../DOM/Nodes/ReactNativeElement'; -import type {InternalInstanceHandle} from '../Renderer/shims/ReactNativeTypes'; import type {NativeIntersectionObserverEntry} from './NativeIntersectionObserver'; import DOMRectReadOnly from '../DOM/Geometry/DOMRectReadOnly'; -import {getPublicInstanceFromInternalInstanceHandle} from '../DOM/Nodes/ReadOnlyNode'; /** * The [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) @@ -29,9 +27,17 @@ export default class IntersectionObserverEntry { // We lazily compute all the properties from the raw entry provided by the // native module, so we avoid unnecessary work. _nativeEntry: NativeIntersectionObserverEntry; - - constructor(nativeEntry: NativeIntersectionObserverEntry) { + // There are cases where this cannot be safely derived from the instance + // handle in the native entry (when the target is detached), so we need to + // keep a reference to it directly. + _target: ReactNativeElement; + + constructor( + nativeEntry: NativeIntersectionObserverEntry, + target: ReactNativeElement, + ) { this._nativeEntry = nativeEntry; + this._target = target; } /** @@ -113,15 +119,7 @@ export default class IntersectionObserverEntry { * The `ReactNativeElement` whose intersection with the root changed. */ get target(): ReactNativeElement { - const targetInstanceHandle: InternalInstanceHandle = - // $FlowExpectedError[incompatible-type] native modules don't support using InternalInstanceHandle as a type - this._nativeEntry.targetInstanceHandle; - - const targetElement = - getPublicInstanceFromInternalInstanceHandle(targetInstanceHandle); - - // $FlowExpectedError[incompatible-cast] we know targetElement is a ReactNativeElement, not just a ReadOnlyNode - return (targetElement: ReactNativeElement); + return this._target; } /** @@ -135,6 +133,7 @@ export default class IntersectionObserverEntry { export function createIntersectionObserverEntry( entry: NativeIntersectionObserverEntry, + target: ReactNativeElement, ): IntersectionObserverEntry { - return new IntersectionObserverEntry(entry); + return new IntersectionObserverEntry(entry, target); } diff --git a/packages/react-native/Libraries/IntersectionObserver/IntersectionObserverManager.js b/packages/react-native/Libraries/IntersectionObserver/IntersectionObserverManager.js index c3f88ee7ef194d..0c39ede1bbf5dc 100644 --- a/packages/react-native/Libraries/IntersectionObserver/IntersectionObserverManager.js +++ b/packages/react-native/Libraries/IntersectionObserver/IntersectionObserverManager.js @@ -24,7 +24,7 @@ import type IntersectionObserver, { } from './IntersectionObserver'; import type IntersectionObserverEntry from './IntersectionObserverEntry'; -import {getShadowNode} from '../DOM/Nodes/ReadOnlyNode'; +import {getInstanceHandle, getShadowNode} from '../DOM/Nodes/ReadOnlyNode'; import * as Systrace from '../Performance/Systrace'; import warnOnce from '../Utilities/warnOnce'; import {createIntersectionObserverEntry} from './IntersectionObserverEntry'; @@ -40,6 +40,36 @@ const registeredIntersectionObservers: Map< {observer: IntersectionObserver, callback: IntersectionObserverCallback}, > = new Map(); +// We need to keep the mapping from instance handles to targets because when +// targets are detached (their components are unmounted), React resets the +// instance handle to prevent memory leaks and it cuts the connection between +// the instance handle and the target. +const instanceHandleToTargetMap: WeakMap = + new WeakMap(); + +function getTargetFromInstanceHandle( + instanceHandle: mixed, +): ?ReactNativeElement { + // $FlowExpectedError[incompatible-type] instanceHandle is typed as mixed but we know it's an object and we need it to be to use it as a key in a WeakMap. + const key: interface {} = instanceHandle; + return instanceHandleToTargetMap.get(key); +} + +function setTargetForInstanceHandle( + instanceHandle: mixed, + target: ReactNativeElement, +): void { + // $FlowExpectedError[incompatible-type] instanceHandle is typed as mixed but we know it's an object and we need it to be to use it as a key in a WeakMap. + const key: interface {} = instanceHandle; + instanceHandleToTargetMap.set(key, target); +} + +function unsetTargetForInstanceHandle(instanceHandle: mixed): void { + // $FlowExpectedError[incompatible-type] instanceHandle is typed as mixed but we know it's an object and we need it to be to use it as a key in a WeakMap. + const key: interface {} = instanceHandle; + instanceHandleToTargetMap.delete(key); +} + /** * Registers the given intersection observer and returns a unique ID for it, * which is required to start observing targets. @@ -109,6 +139,18 @@ export function observe({ return; } + const instanceHandle = getInstanceHandle(target); + if (instanceHandle == null) { + console.error( + 'IntersectionObserverManager: could not find reference to instance handle from target', + ); + return; + } + + // Store the mapping between the instance handle and the target so we can + // access it even after the instance handle has been unmounted. + setTargetForInstanceHandle(instanceHandle, target); + if (!isConnected) { NativeIntersectionObserver.connect(notifyIntersectionObservers); isConnected = true; @@ -148,10 +190,22 @@ export function unobserve( return; } + const instanceHandle = getInstanceHandle(target); + if (instanceHandle == null) { + console.error( + 'IntersectionObserverManager: could not find reference to instance handle from target', + ); + return; + } + NativeIntersectionObserver.unobserve( intersectionObserverId, targetShadowNode, ); + + // We can guarantee we won't receive any more entries for this target, + // so we don't need to keep the mapping anymore. + unsetTargetForInstanceHandle(instanceHandle); } /** @@ -188,7 +242,16 @@ function doNotifyIntersectionObservers(): void { list = []; entriesByObserver.set(nativeEntry.intersectionObserverId, list); } - list.push(createIntersectionObserverEntry(nativeEntry)); + + const target = getTargetFromInstanceHandle( + nativeEntry.targetInstanceHandle, + ); + if (target == null) { + console.warn('Could not find target to create IntersectionObserverEntry'); + continue; + } + + list.push(createIntersectionObserverEntry(nativeEntry, target)); } for (const [