Remove ReactFabricPublicInstance and used definition from ReactNativePrivateInterface (#26437)

## Summary

Now that React Native owns the definition for public instances in Fabric
and ReactNativePrivateInterface provides the methods to create instances
and access private fields (see
https://github.com/facebook/react-native/pull/36570), we can remove the
definitions from React.

After this PR, React Native public instances will be opaque types for
React and it will only handle their creation but not their definition.
This will make RN similar to DOM in how public instances are handled.

This is a new version of #26418 which was closed without merging.

## How did you test this change?

* Existing tests.
* Manually synced the changes in this PR to React Native and tested it
end to end in Meta's infra.
This commit is contained in:
Rubén Norte
2023-03-22 17:54:36 +00:00
committed by GitHub
parent f77099b6f1
commit 9c54b29b44
13 changed files with 96 additions and 455 deletions

View File

@@ -8,10 +8,6 @@
*/
import type {TouchedViewDataAtPoint, ViewConfig} from './ReactNativeTypes';
import {
createPublicInstance,
type ReactFabricHostComponent,
} from './ReactFabricPublicInstance';
import {create, diff} from './ReactNativeAttributePayload';
import {dispatchEvent} from './ReactFabricEventEmitter';
import {
@@ -23,6 +19,8 @@ import {
import {
ReactNativeViewConfigRegistry,
deepFreezeAndThrowOnMutationInDev,
createPublicInstance,
type PublicInstance as ReactNativePublicInstance,
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
const {
@@ -62,12 +60,12 @@ export type Instance = {
// Reference to the React handle (the fiber)
internalInstanceHandle: Object,
// Exposed through refs.
publicInstance: ReactFabricHostComponent,
publicInstance: PublicInstance,
},
};
export type TextInstance = {node: Node, ...};
export type HydratableInstance = Instance | TextInstance;
export type PublicInstance = ReactFabricHostComponent;
export type PublicInstance = ReactNativePublicInstance;
export type Container = number;
export type ChildSet = Object;
export type HostContext = $ReadOnly<{

View File

@@ -1,153 +0,0 @@
/**
* 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<HostComponent<mixed>>,
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);
}

View File

@@ -1,36 +0,0 @@
/**
* 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';
import {getNodeFromInternalInstanceHandle} from './ReactNativePublicCompat';
/**
* 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 getNodeFromPublicInstance(
publicInstance: ReactFabricHostComponent,
): mixed {
if (publicInstance.__internalInstanceHandle == null) {
return null;
}
return getNodeFromInternalInstanceHandle(
publicInstance.__internalInstanceHandle,
);
}

View File

@@ -17,10 +17,12 @@ import {
import getComponentNameFromType from 'shared/getComponentNameFromType';
import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
// Module provided by RN:
import {UIManager} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
import {
UIManager,
getNodeFromPublicInstance,
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
import {enableGetInspectorDataForInstanceInProduction} from 'shared/ReactFeatureFlags';
import {getClosestInstanceFromNode} from './ReactNativeComponentTree';
import {getNodeFromPublicInstance} from './ReactFabricPublicInstanceUtils';
import {getNodeFromInternalInstanceHandle} from './ReactNativePublicCompat';
const emptyObject = {};

View File

@@ -14,6 +14,8 @@ import type {ElementRef, ElementType} from 'react';
import {
UIManager,
legacySendAccessibilityEvent,
getNodeFromPublicInstance,
getNativeTagFromPublicInstance,
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
import {
@@ -23,11 +25,6 @@ import {
import ReactSharedInternals from 'shared/ReactSharedInternals';
import getComponentNameFromType from 'shared/getComponentNameFromType';
import {
getNodeFromPublicInstance,
getNativeTagFromPublicInstance,
} from './ReactFabricPublicInstanceUtils';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
export function findHostInstance_DEPRECATED<TElementType: ElementType>(
@@ -83,6 +80,7 @@ export function findHostInstance_DEPRECATED<TElementType: ElementType>(
// 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.
// $FlowFixMe[incompatible-return]
return hostInstance;
}
@@ -147,9 +145,8 @@ export function findNodeHandle(componentOrHandle: any): ?number {
return hostInstance;
}
// $FlowFixMe[prop-missing] For compatibility with legacy renderer instances
// $FlowFixMe[incompatible-type] For compatibility with legacy renderer instances
if (hostInstance._nativeTag != null) {
// $FlowFixMe[incompatible-return]
return hostInstance._nativeTag;
}

View File

@@ -7,6 +7,8 @@
* @flow strict-local
*/
export opaque type PublicInstance = mixed;
module.exports = {
get BatchedBridge() {
return require('./BatchedBridge.js');
@@ -44,4 +46,13 @@ module.exports = {
get RawEventEmitter() {
return require('./RawEventEmitter').default;
},
get getNativeTagFromPublicInstance() {
return require('./getNativeTagFromPublicInstance').default;
},
get getNodeFromPublicInstance() {
return require('./getNodeFromPublicInstance').default;
},
get createPublicInstance() {
return require('./createPublicInstance').default;
},
};

View File

@@ -0,0 +1,21 @@
/**
* 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
*/
import type {PublicInstance} from './ReactNativePrivateInterface';
export default function createPublicInstance(
tag: number,
viewConfig: mixed,
internalInstanceHandle: mixed,
): PublicInstance {
return {
__nativeTag: tag,
__internalInstanceHandle: internalInstanceHandle,
};
}

View File

@@ -0,0 +1,16 @@
/**
* 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
*/
import type {PublicInstance} from './ReactNativePrivateInterface';
export default function getNativeTagFromPublicInstance(
publicInstance: PublicInstance,
) {
return publicInstance.__nativeTag;
}

View File

@@ -0,0 +1,20 @@
/**
* 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
*/
import type {PublicInstance} from './ReactNativePrivateInterface';
import {getNodeFromInternalInstanceHandle} from '../../../../ReactNativePublicCompat';
export default function getNodeFromPublicInstance(
publicInstance: PublicInstance,
) {
return getNodeFromInternalInstanceHandle(
publicInstance.__internalInstanceHandle,
);
}

View File

@@ -43,9 +43,9 @@ describe('ReactFabric', () => {
require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
.ReactNativeViewConfigRegistry.register;
getNativeTagFromPublicInstance =
require('../ReactFabricPublicInstanceUtils').getNativeTagFromPublicInstance;
require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface').getNativeTagFromPublicInstance;
getNodeFromPublicInstance =
require('../ReactFabricPublicInstanceUtils').getNodeFromPublicInstance;
require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface').getNodeFromPublicInstance;
act = require('internal-test-utils').act;
});

View File

@@ -37,7 +37,7 @@ describe('created with ReactFabric called with ReactNative', () => {
require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
.ReactNativeViewConfigRegistry.register;
getNativeTagFromPublicInstance =
require('../ReactFabricPublicInstanceUtils').getNativeTagFromPublicInstance;
require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface').getNativeTagFromPublicInstance;
});
it('find Fabric instances with the RN renderer', () => {

View File

@@ -1,248 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* @emails react-core
* @jest-environment node
*/
'use strict';
import * as React from 'react';
beforeEach(() => {
jest.resetModules();
jest.restoreAllMocks();
require('react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager');
});
/**
* Renders a sequence of mock views as dictated by `keyLists`. The `keyLists`
* argument is an array of arrays which determines the number of render passes,
* how many views will be rendered in each pass, and what the keys are for each
* of the views.
*
* If an element in `keyLists` is null, the entire root will be unmounted.
*
* The return value is an array of arrays with the resulting refs from rendering
* each corresponding array of keys.
*
* If the corresponding array of keys is null, the returned element at that
* index will also be null.
*/
async function mockRenderKeys(keyLists) {
const ReactFabric = require('react-native-renderer/fabric');
const createReactNativeComponentClass =
require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
.ReactNativeViewConfigRegistry.register;
const act = require('internal-test-utils').act;
const mockContainerTag = 11;
const MockView = createReactNativeComponentClass('RCTMockView', () => ({
validAttributes: {foo: true},
uiViewClassName: 'RCTMockView',
}));
const result = [];
for (let i = 0; i < keyLists.length; i++) {
const keyList = keyLists[i];
if (Array.isArray(keyList)) {
const refs = keyList.map(key => undefined);
await act(() => {
ReactFabric.render(
<MockView>
{keyList.map((key, index) => (
<MockView
key={key}
ref={ref => {
refs[index] = ref;
}}
/>
))}
</MockView>,
mockContainerTag,
);
});
// Clone `refs` to ignore future passes.
result.push([...refs]);
continue;
}
if (keyList == null) {
await act(() => {
ReactFabric.stopSurface(mockContainerTag);
});
result.push(null);
continue;
}
throw new TypeError(
`Invalid 'keyLists' element of type ${typeof keyList}.`,
);
}
return result;
}
describe('blur', () => {
test('blur() invokes TextInputState', async () => {
const {
TextInputState,
} = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface');
const [[fooRef]] = await mockRenderKeys([['foo']]);
fooRef.blur();
expect(TextInputState.blurTextInput.mock.calls).toEqual([[fooRef]]);
});
});
describe('focus', () => {
test('focus() invokes TextInputState', async () => {
const {
TextInputState,
} = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface');
const [[fooRef]] = await mockRenderKeys([['foo']]);
fooRef.focus();
expect(TextInputState.focusTextInput.mock.calls).toEqual([[fooRef]]);
});
});
describe('measure', () => {
test('component.measure(...) invokes callback', async () => {
const [[fooRef]] = await mockRenderKeys([['foo']]);
const callback = jest.fn();
fooRef.measure(callback);
expect(nativeFabricUIManager.measure).toHaveBeenCalledTimes(1);
expect(callback.mock.calls).toEqual([[10, 10, 100, 100, 0, 0]]);
});
test('unmounted.measure(...) does nothing', async () => {
const [[fooRef]] = await mockRenderKeys([['foo'], null]);
const callback = jest.fn();
fooRef.measure(callback);
expect(nativeFabricUIManager.measure).not.toHaveBeenCalled();
expect(callback).not.toHaveBeenCalled();
});
});
describe('measureInWindow', () => {
test('component.measureInWindow(...) invokes callback', async () => {
const [[fooRef]] = await mockRenderKeys([['foo']]);
const callback = jest.fn();
fooRef.measureInWindow(callback);
expect(nativeFabricUIManager.measureInWindow).toHaveBeenCalledTimes(1);
expect(callback.mock.calls).toEqual([[10, 10, 100, 100]]);
});
test('unmounted.measureInWindow(...) does nothing', async () => {
const [[fooRef]] = await mockRenderKeys([['foo'], null]);
const callback = jest.fn();
fooRef.measureInWindow(callback);
expect(nativeFabricUIManager.measureInWindow).not.toHaveBeenCalled();
expect(callback).not.toHaveBeenCalled();
});
});
describe('measureLayout', () => {
test('component.measureLayout(component, ...) invokes callback', async () => {
const [[fooRef, barRef]] = await mockRenderKeys([['foo', 'bar']]);
const successCallback = jest.fn();
const failureCallback = jest.fn();
fooRef.measureLayout(barRef, successCallback, failureCallback);
expect(nativeFabricUIManager.measureLayout).toHaveBeenCalledTimes(1);
expect(successCallback.mock.calls).toEqual([[1, 1, 100, 100]]);
});
test('unmounted.measureLayout(component, ...) does nothing', async () => {
const [[fooRef, barRef]] = await mockRenderKeys([
['foo', 'bar'],
['foo', null],
]);
const successCallback = jest.fn();
const failureCallback = jest.fn();
fooRef.measureLayout(barRef, successCallback, failureCallback);
expect(nativeFabricUIManager.measureLayout).not.toHaveBeenCalled();
expect(successCallback).not.toHaveBeenCalled();
});
test('component.measureLayout(unmounted, ...) does nothing', async () => {
const [[fooRef, barRef]] = await mockRenderKeys([
['foo', 'bar'],
[null, 'bar'],
]);
const successCallback = jest.fn();
const failureCallback = jest.fn();
fooRef.measureLayout(barRef, successCallback, failureCallback);
expect(nativeFabricUIManager.measureLayout).not.toHaveBeenCalled();
expect(successCallback).not.toHaveBeenCalled();
});
test('unmounted.measureLayout(unmounted, ...) does nothing', async () => {
const [[fooRef, barRef]] = await mockRenderKeys([['foo', 'bar'], null]);
const successCallback = jest.fn();
const failureCallback = jest.fn();
fooRef.measureLayout(barRef, successCallback, failureCallback);
expect(nativeFabricUIManager.measureLayout).not.toHaveBeenCalled();
expect(successCallback).not.toHaveBeenCalled();
});
});
describe('unstable_getBoundingClientRect', () => {
test('component.unstable_getBoundingClientRect() returns DOMRect', async () => {
const [[fooRef]] = await mockRenderKeys([['foo']]);
const rect = fooRef.unstable_getBoundingClientRect();
expect(nativeFabricUIManager.getBoundingClientRect).toHaveBeenCalledTimes(
1,
);
expect(rect.toJSON()).toMatchObject({
x: 10,
y: 10,
width: 100,
height: 100,
});
});
test('unmounted.unstable_getBoundingClientRect() returns empty DOMRect', async () => {
const [[fooRef]] = await mockRenderKeys([['foo'], null]);
const rect = fooRef.unstable_getBoundingClientRect();
expect(nativeFabricUIManager.getBoundingClientRect).not.toHaveBeenCalled();
expect(rect.toJSON()).toMatchObject({x: 0, y: 0, width: 0, height: 0});
});
});
describe('setNativeProps', () => {
test('setNativeProps(...) invokes setNativeProps on Fabric UIManager', async () => {
const {
UIManager,
} = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface');
const [[fooRef]] = await mockRenderKeys([['foo']]);
fooRef.setNativeProps({foo: 'baz'});
expect(UIManager.updateView).not.toBeCalled();
expect(nativeFabricUIManager.setNativeProps).toHaveBeenCalledTimes(1);
});
});

View File

@@ -16,6 +16,7 @@ type __MeasureInWindowOnSuccessCallback = any;
type __MeasureLayoutOnSuccessCallback = any;
type __ReactNativeBaseComponentViewConfig = any;
type __ViewConfigGetter = any;
type __ViewConfig = any;
// libdefs cannot actually import. This is supposed to be the type imported
// from 'react-native-renderer/src/legacy-events/TopLevelEventTypes';
@@ -143,6 +144,18 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
emit: (channel: string, event: RawEventEmitterEvent) => string,
...
};
declare export opaque type PublicInstance;
declare export function getNodeFromPublicInstance(
publicInstance: PublicInstance,
): Object;
declare export function getNativeTagFromPublicInstance(
publicInstance: PublicInstance,
): number;
declare export function createPublicInstance(
tag: number,
viewConfig: __ViewConfig,
internalInstanceHandle: mixed,
): PublicInstance;
}
declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore' {