diff --git a/packages/react-native-fantom/src/index.js b/packages/react-native-fantom/src/index.js index 85a3e56fe1d..18a122bf850 100644 --- a/packages/react-native-fantom/src/index.js +++ b/packages/react-native-fantom/src/index.js @@ -616,6 +616,19 @@ export function createShadowNodeReferenceCounter( return NativeFantom.createShadowNodeReferenceCounter(shadowNode); } +/** + * Returns a function that returns the current revision number for the supplied + * element's shadow node. + * + * @param node The node for which to create a revision getter. + */ +export function createShadowNodeRevisionGetter( + node: ReactNativeElement, +): () => ?number { + let shadowNode = getNativeNodeReference(node); + return NativeFantom.createShadowNodeRevisionGetter(shadowNode); +} + /** * Saves a heap snapshot after forcing garbage collection. * diff --git a/packages/react-native/src/private/__tests__/utilities/ShadowNodeRevisionGetter.js b/packages/react-native/src/private/__tests__/utilities/ShadowNodeRevisionGetter.js new file mode 100644 index 00000000000..8d251da8af1 --- /dev/null +++ b/packages/react-native/src/private/__tests__/utilities/ShadowNodeRevisionGetter.js @@ -0,0 +1,49 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import ReactNativeElement from '../../webapis/dom/nodes/ReadOnlyNode'; +import ensureInstance from './ensureInstance'; +import * as Fantom from '@react-native/fantom'; + +function createShadowNodeRevisionGetter( + element: ReactNativeElement, +): () => ?number { + const getRevision = Fantom.createShadowNodeRevisionGetter(element); + return () => { + return getRevision(); + }; +} + +export function createShadowNodeReferenceGetterRef(): [ + () => ?number, + React.RefSetter, +] { + let getRevision: ?() => ?number; + + function getShadowNodeReferenceCount() { + if (getRevision == null) { + throw new Error('ShadowNode revision getter was not initialized.'); + } + return getRevision(); + } + + function ref(instance: mixed | null) { + if (instance == null) { + return; + } + const element = ensureInstance(instance, ReactNativeElement); + if (getRevision != null) { + throw new Error('ShadowNode revision getter was already initialized.'); + } + getRevision = createShadowNodeRevisionGetter(element); + } + + return [getShadowNodeReferenceCount, ref]; +} diff --git a/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeRevisionGetter-itest.js b/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeRevisionGetter-itest.js new file mode 100644 index 00000000000..3475f0cb385 --- /dev/null +++ b/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeRevisionGetter-itest.js @@ -0,0 +1,74 @@ +/** + * 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. + * + * @flow strict-local + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import {createShadowNodeReferenceGetterRef} from '../ShadowNodeRevisionGetter'; +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import {ScrollView, View} from 'react-native'; + +test('base case when cloning results in revision +1', () => { + const root = Fantom.createRoot(); + + const [getRevision, ref] = createShadowNodeReferenceGetterRef(); + + Fantom.runTask(() => { + root.render(); + }); + + expect(getRevision()).toBe(1); + + Fantom.runTask(() => { + root.render(); + }); + + expect(getRevision()).toBe(2); +}); + +test('changing height of the top item in ScrollView results in excessive cloning', () => { + const root = Fantom.createRoot(); + const [getRevision, ref] = createShadowNodeReferenceGetterRef(); + + Fantom.runTask(() => { + root.render( + + + + + + + + + + , + ); + }); + + expect(getRevision()).toBe(1); + + Fantom.runTask(() => { + root.render( + + + + + + + + + + , + ); + }); + + // TODO(T225268793): the below assertion should be: `expect(getRevision()).toBe(1);` + expect(getRevision()).toBe(2); +}); diff --git a/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js b/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js index c3c5a500aa0..10b0d6d77f9 100644 --- a/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js +++ b/packages/react-native/src/private/testing/fantom/specs/NativeFantom.js @@ -96,6 +96,9 @@ interface Spec extends TurboModule { createShadowNodeReferenceCounter( shadowNode: mixed /* ShadowNode */, ): () => number; + createShadowNodeRevisionGetter( + shadowNode: mixed /* ShadowNode */, + ): () => ?number; saveJSMemoryHeapSnapshot: (filePath: string) => void; }