Skip to content

Commit

Permalink
Extracted definition and access to public instances to a separate mod…
Browse files Browse the repository at this point in the history
…ule 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](react-native-community/discussions-and-proposals#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.
  • Loading branch information
rubennorte committed Mar 13, 2023
1 parent cd20376 commit f828bad
Show file tree
Hide file tree
Showing 13 changed files with 392 additions and 213 deletions.
5 changes: 5 additions & 0 deletions packages/react-native-renderer/src/ReactFabric.js
Expand Up @@ -38,6 +38,7 @@ import {
findNodeHandle,
dispatchCommand,
sendAccessibilityEvent,
getNodeFromInternalInstanceHandle,
} from './ReactNativePublicCompat';

// $FlowFixMe[missing-local-annot]
Expand Down Expand Up @@ -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({
Expand Down
Expand Up @@ -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;
}
Expand All @@ -35,7 +41,7 @@ function getNodeFromInstance(fiber: Fiber): PublicInstance {
}

function getFiberCurrentPropsFromNode(instance: Instance): Props {
return instance.canonical.currentProps;
return instance.currentProps;
}

export {
Expand Down
202 changes: 38 additions & 164 deletions packages/react-native-renderer/src/ReactFabricHostConfig.js
Expand Up @@ -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,
Expand All @@ -31,7 +22,6 @@ import {
// Modules provided by RN:
import {
ReactNativeViewConfigRegistry,
TextInputState,
deepFreezeAndThrowOnMutationInDev,
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';

Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<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 = 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<Fn> = $Call<<T>((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';
Expand Down Expand Up @@ -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,
};
}

Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -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,
};
}

Expand All @@ -477,15 +347,19 @@ 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'}},
viewConfig.validAttributes,
);
return {
node: cloneNodeWithNewProps(node, updatePayload),
canonical: instance.canonical,
nativeTag: instance.nativeTag,
viewConfig: instance.viewConfig,
currentProps: instance.currentProps,
internalInstanceHandle: instance.internalInstanceHandle,
publicInstance: instance.publicInstance,
};
}

Expand Down

0 comments on commit f828bad

Please sign in to comment.