From f828bad387f573c462d2a58afb38aea8bdbe9cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 13 Mar 2023 13:25:42 +0000 Subject: [PATCH] Extracted definition and access to public instances to a separate module in Fabric (#26321) ## Summary The current definition of `Instance` in Fabric has 2 fields: - `node`: reference to the native node in the shadow tree. - `canonical`: public instance provided to users via refs + some internal fields needed by Fabric. We're currently using `canonical` not only as the public instance, but also to store internal properties that Fabric needs to access in different parts of the codebase. Those properties are, in fact, available through refs as well, which breaks encapsulation. This PR splits that into 2 separate fields, leaving the definition of instance as: - `node`: reference to the native node in the shadow tree. - `publicInstance`: public instance provided to users via refs. - Rest of internal fields needed by Fabric at the instance level. This also migrates all the current usages of `canonical` to use the right property depending on the use case. To improve encapsulation (and in preparation for the implementation of this [proposal to bring some DOM APIs to public instances in React Native](https://github.com/react-native-community/discussions-and-proposals/pull/607)), this also **moves the creation of and the access to the public instance to separate modules** (`ReactFabricPublicInstance` and `ReactFabricPublicInstanceUtils`). In a following diff, that module will be moved into the `react-native` repository and we'll access it through `ReactNativePrivateInterface`. ## How did you test this change? Existing unit tests. Manually synced the PR in Meta infra and tested in Catalyst + the integration with DevTools. Everything is working normally. --- .../react-native-renderer/src/ReactFabric.js | 5 + .../src/ReactFabricComponentTree.js | 8 +- .../src/ReactFabricHostConfig.js | 202 ++++-------------- .../src/ReactFabricPublicInstance.js | 153 +++++++++++++ .../src/ReactFabricPublicInstanceUtils.js | 29 +++ .../src/ReactNativeComponentTree.js | 5 +- .../src/ReactNativeFiberInspector.js | 34 ++- .../src/ReactNativeHostConfig.js | 4 +- .../src/ReactNativePublicCompat.js | 103 +++++++-- .../src/ReactNativeTypes.js | 3 + .../__tests__/ReactFabric-test.internal.js | 40 +++- .../ReactFabricAndNative-test.internal.js | 5 +- scripts/flow/react-native-host-hooks.js | 14 +- 13 files changed, 392 insertions(+), 213 deletions(-) create mode 100644 packages/react-native-renderer/src/ReactFabricPublicInstance.js create mode 100644 packages/react-native-renderer/src/ReactFabricPublicInstanceUtils.js diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index 84d5651f83be1..a6e2bd883b84f 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -38,6 +38,7 @@ import { findNodeHandle, dispatchCommand, sendAccessibilityEvent, + getNodeFromInternalInstanceHandle, } from './ReactNativePublicCompat'; // $FlowFixMe[missing-local-annot] @@ -119,6 +120,10 @@ export { // This export is typically undefined in production builds. // See the "enableGetInspectorDataForInstanceInProduction" flag. getInspectorDataForInstance, + // The public instance has a reference to the internal instance handle. + // This method allows it to acess the most recent shadow node for + // the instance (it's only accessible through it). + getNodeFromInternalInstanceHandle, }; injectIntoDevTools({ diff --git a/packages/react-native-renderer/src/ReactFabricComponentTree.js b/packages/react-native-renderer/src/ReactFabricComponentTree.js index 4cdb1ca81505c..b1cc8642dee70 100644 --- a/packages/react-native-renderer/src/ReactFabricComponentTree.js +++ b/packages/react-native-renderer/src/ReactFabricComponentTree.js @@ -20,6 +20,12 @@ import {getPublicInstance} from './ReactFabricHostConfig'; // This is ok in DOM because they types are interchangeable, but in React Native // they aren't. function getInstanceFromNode(node: Instance | TextInstance): Fiber | null { + const instance: Instance = (node: $FlowFixMe); // In React Native, node is never a text instance + + if (instance.internalInstanceHandle != null) { + return instance.internalInstanceHandle; + } + // $FlowFixMe[incompatible-return] DevTools incorrectly passes a fiber in React Native. return node; } @@ -35,7 +41,7 @@ function getNodeFromInstance(fiber: Fiber): PublicInstance { } function getFiberCurrentPropsFromNode(instance: Instance): Props { - return instance.canonical.currentProps; + return instance.currentProps; } export { diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 717a496453eda..0b2dfd0ffe91f 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -7,22 +7,13 @@ * @flow */ -import type {ElementRef} from 'react'; -import type { - HostComponent, - MeasureInWindowOnSuccessCallback, - MeasureLayoutOnSuccessCallback, - MeasureOnSuccessCallback, - INativeMethods, - ViewConfig, - TouchedViewDataAtPoint, -} from './ReactNativeTypes'; - -import {warnForStyleProps} from './NativeMethodsMixinUtils'; +import type {TouchedViewDataAtPoint, ViewConfig} from './ReactNativeTypes'; +import { + createPublicInstance, + type ReactFabricHostComponent, +} from './ReactFabricPublicInstance'; import {create, diff} from './ReactNativeAttributePayload'; - import {dispatchEvent} from './ReactFabricEventEmitter'; - import { DefaultEventPriority, DiscreteEventPriority, @@ -31,7 +22,6 @@ import { // Modules provided by RN: import { ReactNativeViewConfigRegistry, - TextInputState, deepFreezeAndThrowOnMutationInDev, } from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; @@ -46,14 +36,9 @@ const { appendChildToSet: appendChildNodeToSet, completeRoot, registerEventHandler, - measure: fabricMeasure, - measureInWindow: fabricMeasureInWindow, - measureLayout: fabricMeasureLayout, unstable_DefaultEventPriority: FabricDefaultPriority, unstable_DiscreteEventPriority: FabricDiscretePriority, unstable_getCurrentEventPriority: fabricGetCurrentEventPriority, - setNativeProps, - getBoundingClientRect: fabricGetBoundingClientRect, } = nativeFabricUIManager; const {get: getViewConfigForType} = ReactNativeViewConfigRegistry; @@ -68,9 +53,15 @@ type Node = Object; export type Type = string; export type Props = Object; export type Instance = { + // Reference to the shadow node. node: Node, - canonical: ReactFabricHostComponent, - ... + nativeTag: number, + viewConfig: ViewConfig, + currentProps: Props, + // Reference to the React handle (the fiber) + internalInstanceHandle: Object, + // Exposed through refs. + publicInstance: ReactFabricHostComponent, }; export type TextInstance = {node: Node, ...}; export type HydratableInstance = Instance | TextInstance; @@ -104,137 +95,6 @@ if (registerEventHandler) { registerEventHandler(dispatchEvent); } -const noop = () => {}; - -/** - * This is used for refs on host components. - */ -class ReactFabricHostComponent implements INativeMethods { - _nativeTag: number; - viewConfig: ViewConfig; - currentProps: Props; - _internalInstanceHandle: Object; - - constructor( - tag: number, - viewConfig: ViewConfig, - props: Props, - internalInstanceHandle: Object, - ) { - this._nativeTag = tag; - this.viewConfig = viewConfig; - this.currentProps = props; - this._internalInstanceHandle = internalInstanceHandle; - } - - blur() { - TextInputState.blurTextInput(this); - } - - focus() { - TextInputState.focusTextInput(this); - } - - measure(callback: MeasureOnSuccessCallback) { - const node = getShadowNodeFromInternalInstanceHandle( - this._internalInstanceHandle, - ); - if (node != null) { - fabricMeasure(node, callback); - } - } - - measureInWindow(callback: MeasureInWindowOnSuccessCallback) { - const node = getShadowNodeFromInternalInstanceHandle( - this._internalInstanceHandle, - ); - if (node != null) { - fabricMeasureInWindow(node, callback); - } - } - - measureLayout( - relativeToNativeNode: number | ElementRef>, - onSuccess: MeasureLayoutOnSuccessCallback, - onFail?: () => void /* currently unused */, - ) { - if ( - typeof relativeToNativeNode === 'number' || - !(relativeToNativeNode instanceof ReactFabricHostComponent) - ) { - if (__DEV__) { - console.error( - 'Warning: ref.measureLayout must be called with a ref to a native component.', - ); - } - - return; - } - - const toStateNode = getShadowNodeFromInternalInstanceHandle( - this._internalInstanceHandle, - ); - const fromStateNode = getShadowNodeFromInternalInstanceHandle( - relativeToNativeNode._internalInstanceHandle, - ); - - if (toStateNode != null && fromStateNode != null) { - fabricMeasureLayout( - toStateNode, - fromStateNode, - onFail != null ? onFail : noop, - onSuccess != null ? onSuccess : noop, - ); - } - } - - unstable_getBoundingClientRect(): DOMRect { - const node = getShadowNodeFromInternalInstanceHandle( - this._internalInstanceHandle, - ); - if (node != null) { - const rect = fabricGetBoundingClientRect(node); - - if (rect) { - return new DOMRect(rect[0], rect[1], rect[2], rect[3]); - } - } - - // Empty rect if any of the above failed - return new DOMRect(0, 0, 0, 0); - } - - setNativeProps(nativeProps: Object) { - if (__DEV__) { - warnForStyleProps(nativeProps, this.viewConfig.validAttributes); - } - const updatePayload = create(nativeProps, this.viewConfig.validAttributes); - - const node = getShadowNodeFromInternalInstanceHandle( - this._internalInstanceHandle, - ); - if (node != null && updatePayload != null) { - setNativeProps(node, updatePayload); - } - } -} - -type ParamOf = $Call<((arg: T) => mixed) => T, Fn>; -type ShadowNode = ParamOf<(typeof nativeFabricUIManager)['measure']>; - -export function getShadowNodeFromInternalInstanceHandle( - internalInstanceHandle: mixed, -): ?ShadowNode { - return ( - // $FlowExpectedError[incompatible-return] internalInstanceHandle is opaque but we need to make an exception here. - internalInstanceHandle && - // $FlowExpectedError[incompatible-return] - internalInstanceHandle.stateNode && - // $FlowExpectedError[incompatible-use] - internalInstanceHandle.stateNode.node - ); -} - export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMutation'; export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration'; export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes'; @@ -280,16 +140,19 @@ export function createInstance( internalInstanceHandle, // internalInstanceHandle ); - const component = new ReactFabricHostComponent( + const component = createPublicInstance( tag, viewConfig, - props, internalInstanceHandle, ); return { node: node, - canonical: component, + nativeTag: tag, + viewConfig, + currentProps: props, + internalInstanceHandle, + publicInstance: component, }; } @@ -359,12 +222,15 @@ export function getChildHostContext( } export function getPublicInstance(instance: Instance): null | PublicInstance { - if (instance.canonical) { - return instance.canonical; + if (instance.publicInstance != null) { + return instance.publicInstance; } - // For compatibility with Paper + // For compatibility with the legacy renderer, in case it's used with Fabric + // in the same app. + // $FlowExpectedError[prop-missing] if (instance._nativeTag != null) { + // $FlowExpectedError[incompatible-return] return instance; } @@ -383,12 +249,12 @@ export function prepareUpdate( newProps: Props, hostContext: HostContext, ): null | Object { - const viewConfig = instance.canonical.viewConfig; + const viewConfig = instance.viewConfig; const updatePayload = diff(oldProps, newProps, viewConfig.validAttributes); // TODO: If the event handlers have changed, we need to update the current props // in the commit phase but there is no host config hook to do it yet. // So instead we hack it by updating it in the render phase. - instance.canonical.currentProps = newProps; + instance.currentProps = newProps; return updatePayload; } @@ -467,7 +333,11 @@ export function cloneInstance( } return { node: clone, - canonical: instance.canonical, + nativeTag: instance.nativeTag, + viewConfig: instance.viewConfig, + currentProps: instance.currentProps, + internalInstanceHandle: instance.internalInstanceHandle, + publicInstance: instance.publicInstance, }; } @@ -477,7 +347,7 @@ export function cloneHiddenInstance( props: Props, internalInstanceHandle: Object, ): Instance { - const viewConfig = instance.canonical.viewConfig; + const viewConfig = instance.viewConfig; const node = instance.node; const updatePayload = create( {style: {display: 'none'}}, @@ -485,7 +355,11 @@ export function cloneHiddenInstance( ); return { node: cloneNodeWithNewProps(node, updatePayload), - canonical: instance.canonical, + nativeTag: instance.nativeTag, + viewConfig: instance.viewConfig, + currentProps: instance.currentProps, + internalInstanceHandle: instance.internalInstanceHandle, + publicInstance: instance.publicInstance, }; } diff --git a/packages/react-native-renderer/src/ReactFabricPublicInstance.js b/packages/react-native-renderer/src/ReactFabricPublicInstance.js new file mode 100644 index 0000000000000..282e5af7cde1e --- /dev/null +++ b/packages/react-native-renderer/src/ReactFabricPublicInstance.js @@ -0,0 +1,153 @@ +/** + * 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 + */ + +import type {ElementRef} from 'react'; +import type { + ViewConfig, + INativeMethods, + HostComponent, + MeasureInWindowOnSuccessCallback, + MeasureLayoutOnSuccessCallback, + MeasureOnSuccessCallback, +} from './ReactNativeTypes'; + +import {TextInputState} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; +import {create} from './ReactNativeAttributePayload'; +import {warnForStyleProps} from './NativeMethodsMixinUtils'; +import {getNodeFromInternalInstanceHandle} from './ReactNativePublicCompat'; + +const { + measure: fabricMeasure, + measureInWindow: fabricMeasureInWindow, + measureLayout: fabricMeasureLayout, + setNativeProps, + getBoundingClientRect: fabricGetBoundingClientRect, +} = nativeFabricUIManager; + +const noop = () => {}; + +/** + * This is used for refs on host components. + */ +export class ReactFabricHostComponent implements INativeMethods { + // These need to be accessible from `ReactFabricPublicInstanceUtils`. + __nativeTag: number; + __internalInstanceHandle: mixed; + + _viewConfig: ViewConfig; + + constructor( + tag: number, + viewConfig: ViewConfig, + internalInstanceHandle: mixed, + ) { + this.__nativeTag = tag; + this._viewConfig = viewConfig; + this.__internalInstanceHandle = internalInstanceHandle; + } + + blur() { + TextInputState.blurTextInput(this); + } + + focus() { + TextInputState.focusTextInput(this); + } + + measure(callback: MeasureOnSuccessCallback) { + const node = getNodeFromInternalInstanceHandle( + this.__internalInstanceHandle, + ); + if (node != null) { + fabricMeasure(node, callback); + } + } + + measureInWindow(callback: MeasureInWindowOnSuccessCallback) { + const node = getNodeFromInternalInstanceHandle( + this.__internalInstanceHandle, + ); + if (node != null) { + fabricMeasureInWindow(node, callback); + } + } + + measureLayout( + relativeToNativeNode: number | ElementRef>, + onSuccess: MeasureLayoutOnSuccessCallback, + onFail?: () => void /* currently unused */, + ) { + if ( + typeof relativeToNativeNode === 'number' || + !(relativeToNativeNode instanceof ReactFabricHostComponent) + ) { + if (__DEV__) { + console.error( + 'Warning: ref.measureLayout must be called with a ref to a native component.', + ); + } + + return; + } + + const toStateNode = getNodeFromInternalInstanceHandle( + this.__internalInstanceHandle, + ); + const fromStateNode = getNodeFromInternalInstanceHandle( + relativeToNativeNode.__internalInstanceHandle, + ); + + if (toStateNode != null && fromStateNode != null) { + fabricMeasureLayout( + toStateNode, + fromStateNode, + onFail != null ? onFail : noop, + onSuccess != null ? onSuccess : noop, + ); + } + } + + unstable_getBoundingClientRect(): DOMRect { + const node = getNodeFromInternalInstanceHandle( + this.__internalInstanceHandle, + ); + if (node != null) { + const rect = fabricGetBoundingClientRect(node); + + if (rect) { + return new DOMRect(rect[0], rect[1], rect[2], rect[3]); + } + } + + // Empty rect if any of the above failed + return new DOMRect(0, 0, 0, 0); + } + + setNativeProps(nativeProps: {...}): void { + if (__DEV__) { + warnForStyleProps(nativeProps, this._viewConfig.validAttributes); + } + const updatePayload = create(nativeProps, this._viewConfig.validAttributes); + + const node = getNodeFromInternalInstanceHandle( + this.__internalInstanceHandle, + ); + if (node != null && updatePayload != null) { + setNativeProps(node, updatePayload); + } + } +} + +export function createPublicInstance( + tag: number, + viewConfig: ViewConfig, + internalInstanceHandle: mixed, +): ReactFabricHostComponent { + return new ReactFabricHostComponent(tag, viewConfig, internalInstanceHandle); +} diff --git a/packages/react-native-renderer/src/ReactFabricPublicInstanceUtils.js b/packages/react-native-renderer/src/ReactFabricPublicInstanceUtils.js new file mode 100644 index 0000000000000..a1d9fbea4dcc5 --- /dev/null +++ b/packages/react-native-renderer/src/ReactFabricPublicInstanceUtils.js @@ -0,0 +1,29 @@ +/** + * 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 + */ + +import type {ReactFabricHostComponent} from './ReactFabricPublicInstance'; + +/** + * IMPORTANT: This module is used in Paper and Fabric. It needs to be defined + * outside of `ReactFabricPublicInstance` because that module requires + * `nativeFabricUIManager` to be defined in the global scope (which does not + * happen in Paper). + */ + +export function getNativeTagFromPublicInstance( + publicInstance: ReactFabricHostComponent, +): number { + return publicInstance.__nativeTag; +} + +export function getInternalInstanceHandleFromPublicInstance( + publicInstance: ReactFabricHostComponent, +): mixed { + return publicInstance.__internalInstanceHandle; +} diff --git a/packages/react-native-renderer/src/ReactNativeComponentTree.js b/packages/react-native-renderer/src/ReactNativeComponentTree.js index 3af3fa5a443f5..7021610daf20d 100644 --- a/packages/react-native-renderer/src/ReactNativeComponentTree.js +++ b/packages/react-native-renderer/src/ReactNativeComponentTree.js @@ -25,8 +25,9 @@ function getTagFromInstance(inst) { let nativeInstance = inst.stateNode; let tag = nativeInstance._nativeTag; if (tag === undefined) { - nativeInstance = nativeInstance.canonical; - tag = nativeInstance._nativeTag; + // For compatibility with Fabric + tag = nativeInstance.nativeTag; + nativeInstance = nativeInstance.publicInstance; } if (!tag) { diff --git a/packages/react-native-renderer/src/ReactNativeFiberInspector.js b/packages/react-native-renderer/src/ReactNativeFiberInspector.js index 7641e9314f00f..696e26baef800 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberInspector.js +++ b/packages/react-native-renderer/src/ReactNativeFiberInspector.js @@ -20,6 +20,8 @@ import {HostComponent} from 'react-reconciler/src/ReactWorkTags'; import {UIManager} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; import {enableGetInspectorDataForInstanceInProduction} from 'shared/ReactFeatureFlags'; import {getClosestInstanceFromNode} from './ReactNativeComponentTree'; +import {getInternalInstanceHandleFromPublicInstance} from './ReactFabricPublicInstanceUtils'; +import {getNodeFromInternalInstanceHandle} from './ReactNativePublicCompat'; const emptyObject = {}; if (__DEV__) { @@ -46,15 +48,15 @@ if (__DEV__ || enableGetInspectorDataForInstanceInProduction) { props: getHostProps(fiber), source: fiber._debugSource, measure: callback => { - // If this is Fabric, we'll find a ShadowNode and use that to measure. + // If this is Fabric, we'll find a shadow node and use that to measure. const hostFiber = findCurrentHostFiber(fiber); - const shadowNode = + const node = hostFiber != null && hostFiber.stateNode !== null && hostFiber.stateNode.node; - if (shadowNode) { - nativeFabricUIManager.measure(shadowNode, callback); + if (node) { + nativeFabricUIManager.measure(node, callback); } else { return UIManager.measure( getHostNode(fiber, findNodeHandle), @@ -198,30 +200,40 @@ if (__DEV__) { ): void { let closestInstance = null; - if (inspectedView._internalInstanceHandle != null) { + const fabricInstanceHandle = + getInternalInstanceHandleFromPublicInstance(inspectedView); + const fabricNode = + fabricInstanceHandle != null + ? getNodeFromInternalInstanceHandle(fabricInstanceHandle) + : null; + if (fabricNode) { // For Fabric we can look up the instance handle directly and measure it. nativeFabricUIManager.findNodeAtPoint( - inspectedView._internalInstanceHandle.stateNode.node, + fabricNode, locationX, locationY, internalInstanceHandle => { - if (internalInstanceHandle == null) { + const node = + internalInstanceHandle != null + ? getNodeFromInternalInstanceHandle(internalInstanceHandle) + : null; + if (internalInstanceHandle == null || node == null) { callback({ pointerY: locationY, frame: {left: 0, top: 0, width: 0, height: 0}, ...getInspectorDataForInstance(closestInstance), }); + return; } closestInstance = - internalInstanceHandle.stateNode.canonical._internalInstanceHandle; + internalInstanceHandle.stateNode.internalInstanceHandle; // Note: this is deprecated and we want to remove it ASAP. Keeping it here for React DevTools compatibility for now. - const nativeViewTag = - internalInstanceHandle.stateNode.canonical._nativeTag; + const nativeViewTag = internalInstanceHandle.stateNode.nativeTag; nativeFabricUIManager.measure( - internalInstanceHandle.stateNode.node, + node, (x, y, width, height, pageX, pageY) => { const inspectorData = getInspectorDataForInstance(closestInstance); diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index a3b58315205f3..330875debaefd 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -218,8 +218,8 @@ export function getChildHostContext( export function getPublicInstance(instance: Instance): * { // $FlowExpectedError[prop-missing] For compatibility with Fabric - if (instance.canonical) { - return instance.canonical; + if (instance.publicInstance != null) { + return instance.publicInstance; } return instance; diff --git a/packages/react-native-renderer/src/ReactNativePublicCompat.js b/packages/react-native-renderer/src/ReactNativePublicCompat.js index a6be670d912cb..527983af6d593 100644 --- a/packages/react-native-renderer/src/ReactNativePublicCompat.js +++ b/packages/react-native-renderer/src/ReactNativePublicCompat.js @@ -7,7 +7,7 @@ * @flow */ -import type {HostComponent} from './ReactNativeTypes'; +import type {Node, HostComponent} from './ReactNativeTypes'; import type {ElementRef, ElementType} from 'react'; // Modules provided by RN: @@ -23,6 +23,11 @@ import { import ReactSharedInternals from 'shared/ReactSharedInternals'; import getComponentNameFromType from 'shared/getComponentNameFromType'; +import { + getInternalInstanceHandleFromPublicInstance, + getNativeTagFromPublicInstance, +} from './ReactFabricPublicInstanceUtils'; + const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; export function findHostInstance_DEPRECATED( @@ -45,19 +50,24 @@ export function findHostInstance_DEPRECATED( owner.stateNode._warnedAboutRefsInRender = true; } } + if (componentOrHandle == null) { return null; } - // $FlowFixMe Flow has hardcoded values for React DOM that don't work with RN + + // For compatibility with Fabric instances + if (componentOrHandle.publicInstance) { + // $FlowExpectedError[incompatible-return] Can't refine componentOrHandle as a Fabric instance + return componentOrHandle.publicInstance; + } + + // For compatibility with legacy renderer instances if (componentOrHandle._nativeTag) { - // $FlowFixMe Flow has hardcoded values for React DOM that don't work with RN + // $FlowFixMe[incompatible-exact] Necessary when running Flow on Fabric + // $FlowFixMe[incompatible-return] return componentOrHandle; } - // $FlowFixMe Flow has hardcoded values for React DOM that don't work with RN - if (componentOrHandle.canonical && componentOrHandle.canonical._nativeTag) { - // $FlowFixMe Flow has hardcoded values for React DOM that don't work with RN - return componentOrHandle.canonical; - } + let hostInstance; if (__DEV__) { hostInstance = findHostInstanceWithWarning( @@ -68,6 +78,7 @@ export function findHostInstance_DEPRECATED( hostInstance = findHostInstance(componentOrHandle); } + // findHostInstance handles legacy vs. Fabric differences correctly // $FlowFixMe[incompatible-exact] we need to fix the definition of `HostComponent` to use NativeMethods as an interface, not as a type. return hostInstance; } @@ -90,19 +101,32 @@ export function findNodeHandle(componentOrHandle: any): ?number { owner.stateNode._warnedAboutRefsInRender = true; } } + if (componentOrHandle == null) { return null; } + if (typeof componentOrHandle === 'number') { // Already a node handle return componentOrHandle; } + + // For compatibility with legacy renderer instances if (componentOrHandle._nativeTag) { return componentOrHandle._nativeTag; } - if (componentOrHandle.canonical && componentOrHandle.canonical._nativeTag) { - return componentOrHandle.canonical._nativeTag; + + // For compatibility with Fabric instances + if (componentOrHandle.nativeTag != null) { + return componentOrHandle.nativeTag; } + + // For compatibility with Fabric public instances + const nativeTag = getNativeTagFromPublicInstance(componentOrHandle); + if (nativeTag) { + return nativeTag; + } + let hostInstance; if (__DEV__) { hostInstance = findHostInstanceWithWarning( @@ -117,7 +141,14 @@ export function findNodeHandle(componentOrHandle: any): ?number { return hostInstance; } - return hostInstance._nativeTag; + // $FlowFixMe[prop-missing] For compatibility with legacy renderer instances + if (hostInstance._nativeTag != null) { + // $FlowFixMe[incompatible-return] + return hostInstance._nativeTag; + } + + // $FlowFixMe[incompatible-call] Necessary when running Flow on the legacy renderer + return getNativeTagFromPublicInstance(hostInstance); } export function dispatchCommand( @@ -125,7 +156,11 @@ export function dispatchCommand( command: string, args: Array, ) { - if (handle._nativeTag == null) { + const nativeTag = + handle._nativeTag != null + ? handle._nativeTag + : getNativeTagFromPublicInstance(handle); + if (nativeTag == null) { if (__DEV__) { console.error( "dispatchCommand was called with a ref that isn't a " + @@ -135,18 +170,25 @@ export function dispatchCommand( return; } - if (handle._internalInstanceHandle != null) { - const {stateNode} = handle._internalInstanceHandle; - if (stateNode != null) { - nativeFabricUIManager.dispatchCommand(stateNode.node, command, args); + const internalInstanceHandle = + getInternalInstanceHandleFromPublicInstance(handle); + + if (internalInstanceHandle != null) { + const node = getNodeFromInternalInstanceHandle(internalInstanceHandle); + if (node != null) { + nativeFabricUIManager.dispatchCommand(node, command, args); } } else { - UIManager.dispatchViewManagerCommand(handle._nativeTag, command, args); + UIManager.dispatchViewManagerCommand(nativeTag, command, args); } } export function sendAccessibilityEvent(handle: any, eventType: string) { - if (handle._nativeTag == null) { + const nativeTag = + handle._nativeTag != null + ? handle._nativeTag + : getNativeTagFromPublicInstance(handle); + if (nativeTag == null) { if (__DEV__) { console.error( "sendAccessibilityEvent was called with a ref that isn't a " + @@ -156,12 +198,27 @@ export function sendAccessibilityEvent(handle: any, eventType: string) { return; } - if (handle._internalInstanceHandle != null) { - const {stateNode} = handle._internalInstanceHandle; - if (stateNode != null) { - nativeFabricUIManager.sendAccessibilityEvent(stateNode.node, eventType); + const internalInstanceHandle = + getInternalInstanceHandleFromPublicInstance(handle); + if (internalInstanceHandle != null) { + const node = getNodeFromInternalInstanceHandle(internalInstanceHandle); + if (node != null) { + nativeFabricUIManager.sendAccessibilityEvent(node, eventType); } } else { - legacySendAccessibilityEvent(handle._nativeTag, eventType); + legacySendAccessibilityEvent(nativeTag, eventType); } } + +export function getNodeFromInternalInstanceHandle( + internalInstanceHandle: mixed, +): ?Node { + return ( + // $FlowExpectedError[incompatible-return] internalInstanceHandle is opaque but we need to make an exception here. + internalInstanceHandle && + // $FlowExpectedError[incompatible-return] + internalInstanceHandle.stateNode && + // $FlowExpectedError[incompatible-use] + internalInstanceHandle.stateNode.node + ); +} diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index a524d4313866c..ca9a9dfc04d03 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -212,6 +212,8 @@ export type ReactNativeType = { ... }; +export opaque type Node = mixed; + export type ReactFabricType = { findHostInstance_DEPRECATED( componentOrHandle: ?(ElementRef | number), @@ -235,6 +237,7 @@ export type ReactFabricType = { concurrentRoot: ?boolean, ): ?ElementRef, unmountComponentAtNode(containerTag: number): void, + getNodeFromInternalInstanceHandle(internalInstanceHandle: mixed): ?Node, ... }; diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index 935edb0335def..42ed69a8c290c 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -15,6 +15,8 @@ let ReactFabric; let createReactNativeComponentClass; let StrictMode; let act; +let getNativeTagFromPublicInstance; +let getInternalInstanceHandleFromPublicInstance; const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT = "Warning: dispatchCommand was called with a ref that isn't a " + @@ -40,6 +42,10 @@ describe('ReactFabric', () => { createReactNativeComponentClass = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface') .ReactNativeViewConfigRegistry.register; + getNativeTagFromPublicInstance = + require('../ReactFabricPublicInstanceUtils').getNativeTagFromPublicInstance; + getInternalInstanceHandleFromPublicInstance = + require('../ReactFabricPublicInstanceUtils').getInternalInstanceHandleFromPublicInstance; act = require('internal-test-utils').act; }); @@ -931,7 +937,7 @@ describe('ReactFabric', () => { '\n in RCTView (at **)' + '\n in ContainsStrictModeChild (at **)', ]); - expect(match).toBe(child._nativeTag); + expect(match).toBe(getNativeTagFromPublicInstance(child)); }); it('findNodeHandle should warn if passed a component that is inside StrictMode', async () => { @@ -968,7 +974,7 @@ describe('ReactFabric', () => { '\n in RCTView (at **)' + '\n in IsInStrictMode (at **)', ]); - expect(match).toBe(child._nativeTag); + expect(match).toBe(getNativeTagFromPublicInstance(child)); }); it('should no-op if calling sendAccessibilityEvent on unmounted refs', async () => { @@ -1002,4 +1008,34 @@ describe('ReactFabric', () => { expect(nativeFabricUIManager.sendAccessibilityEvent).not.toBeCalled(); }); + + it('getNodeFromInternalInstanceHandle should return the correct shadow node', async () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + let viewRef; + await act(() => { + ReactFabric.render( + { + viewRef = ref; + }} + />, + 1, + ); + }); + + const expectedShadowNode = + nativeFabricUIManager.createNode.mock.results[0].value; + expect(expectedShadowNode).toEqual(expect.any(Object)); + + const internalInstanceHandle = + getInternalInstanceHandleFromPublicInstance(viewRef); + expect( + ReactFabric.getNodeFromInternalInstanceHandle(internalInstanceHandle), + ).toBe(expectedShadowNode); + }); }); diff --git a/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js index e494701dd0038..fca6d00c72774 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js @@ -16,6 +16,7 @@ let ReactNative; let UIManager; let createReactNativeComponentClass; let ReactNativePrivateInterface; +let getNativeTagFromPublicInstance; describe('created with ReactFabric called with ReactNative', () => { beforeEach(() => { @@ -35,6 +36,8 @@ describe('created with ReactFabric called with ReactNative', () => { createReactNativeComponentClass = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface') .ReactNativeViewConfigRegistry.register; + getNativeTagFromPublicInstance = + require('../ReactFabricPublicInstanceUtils').getNativeTagFromPublicInstance; }); it('find Fabric instances with the RN renderer', () => { @@ -54,7 +57,7 @@ describe('created with ReactFabric called with ReactNative', () => { ReactFabric.render(, 11); const instance = ReactNative.findHostInstance_DEPRECATED(ref.current); - expect(instance._nativeTag).toBe(2); + expect(getNativeTagFromPublicInstance(instance)).toBe(2); }); it('find Fabric nodes with the RN renderer', () => { diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index e3899db44528d..7cb3e389126c5 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -178,19 +178,19 @@ declare var nativeFabricUIManager: { dispatchCommand: (node: Object, command: string, args: Array) => void, sendAccessibilityEvent: (node: Object, eventTypeName: string) => void, - measure: (node: Node, callback: __MeasureOnSuccessCallback) => void, + measure: (node: Object, callback: __MeasureOnSuccessCallback) => void, measureInWindow: ( - node: Node, + node: Object, callback: __MeasureInWindowOnSuccessCallback, ) => void, measureLayout: ( - node: Node, - relativeNode: Node, + node: Object, + relativeNode: Object, onFail: () => void, onSuccess: __MeasureLayoutOnSuccessCallback, ) => void, getBoundingClientRect: ( - node: Node, + node: Object, ) => [ /* x:*/ number, /* y:*/ number, @@ -198,13 +198,13 @@ declare var nativeFabricUIManager: { /* height:*/ number, ], findNodeAtPoint: ( - node: Node, + node: Object, locationX: number, locationY: number, callback: (Object) => void, ) => void, setIsJSResponder: ( - node: Node, + node: Object, isJsResponder: boolean, blockNativeResponder: boolean, ) => void,