Skip to content

Commit

Permalink
Implement ReactNativeDocument (facebook#49012)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: facebook#49012

Changelog: [internal]

(This is internal for now, until we rollout the DOM APIs in stable).

This refines the concept of root elements from the merged proposal for [DOM Traversal & Layout APIs](https://github.com/react-native-community/discussions-and-proposals/blob/main/proposals/0607-dom-traversal-and-layout-apis.md).

The original proposal included a reference to have the root node in the tree as `getRootNode()` and no other methods/accessors to access it.

This makes the following changes:
* The root node is a new abstraction in React Native implementing the concept of `Document` from Web. `node.getRootNode()`, as well as `node.ownerDocument` now return instances to this node (except when the node is detached, in which case `getRootNode` returns the node itself, aligning with the spec).
* The existing root node in the shadow tree is exposed as the `documentElement` of the new document instance. It would be the first and only child of the document instance, and the topmost parent of all the host nodes rendered in the tree.

In terms of APIs:
* Implements `getRootNode` correctly, according to the specified semantics.
* Adds `ownerDocument` to the `ReadOnlyNode` interface.
* Adds the `ReactNativeDocument` interface, which extends `ReadOnlyNode` (with no new methods on its own, which will be added in a following PR).

NOTE: This is currently gated under `ReactNativeFeatureFlags.enableDOMDocumentAPI` feature flag, which is disabled by default.

Reviewed By: yungsters

Differential Revision: D67526381
  • Loading branch information
rubennorte authored and facebook-github-bot committed Jan 30, 2025
1 parent a7d9a29 commit 8d2c98e
Show file tree
Hide file tree
Showing 25 changed files with 2,246 additions and 147 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @flow strict-local
* @format
*/

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,76 +13,117 @@
* instances and get some data from them (like their instance handle / fiber).
*/

import type ReactNativeElement from '../../../src/private/webapis/dom/nodes/ReactNativeElement';
import type ReadOnlyText from '../../../src/private/webapis/dom/nodes/ReadOnlyText';
import type ReactNativeDocumentT from '../../../src/private/webapis/dom/nodes/ReactNativeDocument';
import typeof * as ReactNativeDocumentModuleT from '../../../src/private/webapis/dom/nodes/ReactNativeDocument';
import type ReactNativeElementT from '../../../src/private/webapis/dom/nodes/ReactNativeElement';
import type ReadOnlyTextT from '../../../src/private/webapis/dom/nodes/ReadOnlyText';
import typeof * as RendererProxyT from '../../ReactNative/RendererProxy';
import type {
InternalInstanceHandle,
Node,
PublicRootInstance,
ViewConfig,
} from '../../Renderer/shims/ReactNativeTypes';
import type {RootTag} from '../RootTag';
import type ReactFabricHostComponent from './ReactFabricHostComponent';
import type ReactFabricHostComponentT from './ReactFabricHostComponent';

import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags';

// Lazy loaded to avoid evaluating the module when using the legacy renderer.
let PublicInstanceClass:
| Class<ReactFabricHostComponent>
| Class<ReactNativeElement>;
let ReadOnlyTextClass: Class<ReadOnlyText>;

// Lazy loaded to avoid evaluating the module when using the legacy renderer.
let ReactNativeDocumentModuleObject: ?ReactNativeDocumentModuleT;
let ReactFabricHostComponentClass: Class<ReactFabricHostComponentT>;
let ReactNativeElementClass: Class<ReactNativeElementT>;
let ReadOnlyTextClass: Class<ReadOnlyTextT>;
let RendererProxy: RendererProxyT;

// This is just a temporary placeholder so ReactFabric doesn't crash when synced.
type PublicRootInstance = null;
function getReactNativeDocumentModule(): ReactNativeDocumentModuleT {
if (ReactNativeDocumentModuleObject == null) {
// We initialize this lazily to avoid a require cycle.
ReactNativeDocumentModuleObject = require('../../../src/private/webapis/dom/nodes/ReactNativeDocument');
}

return ReactNativeDocumentModuleObject;
}

function getReactNativeElementClass(): Class<ReactNativeElementT> {
if (ReactNativeElementClass == null) {
ReactNativeElementClass =
require('../../../src/private/webapis/dom/nodes/ReactNativeElement').default;
}
return ReactNativeElementClass;
}

function getReactFabricHostComponentClass(): Class<ReactFabricHostComponentT> {
if (ReactFabricHostComponentClass == null) {
ReactFabricHostComponentClass =
require('./ReactFabricHostComponent').default;
}
return ReactFabricHostComponentClass;
}

function getReadOnlyTextClass(): Class<ReadOnlyTextT> {
if (ReadOnlyTextClass == null) {
ReadOnlyTextClass =
require('../../../src/private/webapis/dom/nodes/ReadOnlyText').default;
}
return ReadOnlyTextClass;
}

export function createPublicRootInstance(rootTag: RootTag): PublicRootInstance {
// This is just a placeholder so ReactFabric doesn't crash when synced.
if (
ReactNativeFeatureFlags.enableAccessToHostTreeInFabric() &&
ReactNativeFeatureFlags.enableDOMDocumentAPI()
) {
const ReactNativeDocumentModule = getReactNativeDocumentModule();

// $FlowExpectedError[incompatible-return]
return ReactNativeDocumentModule.createReactNativeDocument(rootTag);
}

// $FlowExpectedError[incompatible-return]
return null;
}

export function createPublicInstance(
tag: number,
viewConfig: ViewConfig,
internalInstanceHandle: InternalInstanceHandle,
ownerDocument: PublicRootInstance,
): ReactFabricHostComponent | ReactNativeElement {
if (PublicInstanceClass == null) {
// We don't use inline requires in react-native, so this forces lazy loading
// the right module to avoid eagerly loading both.
if (ReactNativeFeatureFlags.enableAccessToHostTreeInFabric()) {
PublicInstanceClass =
require('../../../src/private/webapis/dom/nodes/ReactNativeElement').default;
} else {
PublicInstanceClass = require('./ReactFabricHostComponent').default;
}
ownerDocument: ReactNativeDocumentT,
): ReactFabricHostComponentT | ReactNativeElementT {
if (ReactNativeFeatureFlags.enableAccessToHostTreeInFabric()) {
const ReactNativeElement = getReactNativeElementClass();
return new ReactNativeElement(
tag,
viewConfig,
internalInstanceHandle,
ownerDocument,
);
} else {
const ReactFabricHostComponent = getReactFabricHostComponentClass();
return new ReactFabricHostComponent(
tag,
viewConfig,
internalInstanceHandle,
);
}

return new PublicInstanceClass(tag, viewConfig, internalInstanceHandle);
}

export function createPublicTextInstance(
internalInstanceHandle: InternalInstanceHandle,
ownerDocument: PublicRootInstance,
): ReadOnlyText {
if (ReadOnlyTextClass == null) {
ReadOnlyTextClass =
require('../../../src/private/webapis/dom/nodes/ReadOnlyText').default;
}

return new ReadOnlyTextClass(internalInstanceHandle);
ownerDocument: ReactNativeDocumentT,
): ReadOnlyTextT {
const ReadOnlyText = getReadOnlyTextClass();
return new ReadOnlyText(internalInstanceHandle, ownerDocument);
}

export function getNativeTagFromPublicInstance(
publicInstance: ReactFabricHostComponent | ReactNativeElement,
publicInstance: ReactFabricHostComponentT | ReactNativeElementT,
): number {
return publicInstance.__nativeTag;
}

export function getNodeFromPublicInstance(
publicInstance: ReactFabricHostComponent | ReactNativeElement,
publicInstance: ReactFabricHostComponentT | ReactNativeElementT,
): ?Node {
// Avoid loading ReactFabric if using an instance from the legacy renderer.
if (publicInstance.__internalInstanceHandle == null) {
Expand All @@ -93,12 +134,13 @@ export function getNodeFromPublicInstance(
RendererProxy = require('../../ReactNative/RendererProxy');
}
return RendererProxy.getNodeFromInternalInstanceHandle(
// $FlowExpectedError[incompatible-call] __internalInstanceHandle is always an InternalInstanceHandle from React when we get here.
publicInstance.__internalInstanceHandle,
);
}

export function getInternalInstanceHandleFromPublicInstance(
publicInstance: ReactFabricHostComponent | ReactNativeElement,
publicInstance: ReactFabricHostComponentT | ReactNativeElementT,
): InternalInstanceHandle {
// TODO(T174762768): Remove this once OSS versions of renderers will be synced.
// $FlowExpectedError[prop-missing] Keeping this for backwards-compatibility with the renderers versions in open source.
Expand All @@ -107,5 +149,6 @@ export function getInternalInstanceHandleFromPublicInstance(
return publicInstance._internalInstanceHandle;
}

// $FlowExpectedError[incompatible-return] __internalInstanceHandle is always an InternalInstanceHandle from React when we get here.
return publicInstance.__internalInstanceHandle;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import '../../../Core/InitializeCore.js';
import type ReactNativeDocument from '../../../../src/private/webapis/dom/nodes/ReactNativeDocument';
import type {
InternalInstanceHandle,
ViewConfig,
Expand All @@ -29,14 +30,20 @@ const viewConfig: ViewConfig = {
};
// $FlowExpectedError[incompatible-type]
const internalInstanceHandle: InternalInstanceHandle = {};
// $FlowExpectedError[incompatible-type]
const ownerDocument: ReactNativeDocument = {};

/* eslint-disable no-new */
Fantom.unstable_benchmark
.suite('ReactNativeElement vs. ReactFabricHostComponent')
.add('ReactNativeElement', () => {
// eslint-disable-next-line no-new
new ReactNativeElement(tag, viewConfig, internalInstanceHandle);
new ReactNativeElement(
tag,
viewConfig,
internalInstanceHandle,
ownerDocument,
);
})
.add('ReactFabricHostComponent', () => {
// eslint-disable-next-line no-new
new ReactFabricHostComponent(tag, viewConfig, internalInstanceHandle);
});
Original file line number Diff line number Diff line change
Expand Up @@ -7508,28 +7508,27 @@ exports[`public API should not change unintentionally Libraries/ReactNative/Reac
`;

exports[`public API should not change unintentionally Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance.js 1`] = `
"type PublicRootInstance = null;
declare export function createPublicRootInstance(
"declare export function createPublicRootInstance(
rootTag: RootTag
): PublicRootInstance;
declare export function createPublicInstance(
tag: number,
viewConfig: ViewConfig,
internalInstanceHandle: InternalInstanceHandle,
ownerDocument: PublicRootInstance
): ReactFabricHostComponent | ReactNativeElement;
ownerDocument: ReactNativeDocumentT
): ReactFabricHostComponentT | ReactNativeElementT;
declare export function createPublicTextInstance(
internalInstanceHandle: InternalInstanceHandle,
ownerDocument: PublicRootInstance
): ReadOnlyText;
ownerDocument: ReactNativeDocumentT
): ReadOnlyTextT;
declare export function getNativeTagFromPublicInstance(
publicInstance: ReactFabricHostComponent | ReactNativeElement
publicInstance: ReactFabricHostComponentT | ReactNativeElementT
): number;
declare export function getNodeFromPublicInstance(
publicInstance: ReactFabricHostComponent | ReactNativeElement
publicInstance: ReactFabricHostComponentT | ReactNativeElementT
): ?Node;
declare export function getInternalInstanceHandleFromPublicInstance(
publicInstance: ReactFabricHostComponent | ReactNativeElement
publicInstance: ReactFabricHostComponentT | ReactNativeElementT
): InternalInstanceHandle;
"
`;
Expand Down Expand Up @@ -11317,18 +11316,38 @@ declare export default class DOMRectReadOnly {
"
`;

exports[`public API should not change unintentionally src/private/webapis/dom/nodes/ReactNativeDocument.js 1`] = `
"declare export default class ReactNativeDocument extends ReadOnlyNode {
_documentElement: ReactNativeElement;
constructor(
rootTag: RootTag,
instanceHandle: ReactNativeDocumentInstanceHandle
): void;
get documentElement(): ReactNativeElement;
get nodeName(): string;
get nodeType(): number;
get nodeValue(): null;
get textContent(): null;
}
declare export function createReactNativeDocument(
rootTag: RootTag
): ReactNativeDocument;
"
`;

exports[`public API should not change unintentionally src/private/webapis/dom/nodes/ReactNativeElement.js 1`] = `
"declare class ReactNativeElementMethods
extends ReadOnlyElement
implements INativeMethods
{
__nativeTag: number;
__internalInstanceHandle: InternalInstanceHandle;
__internalInstanceHandle: InstanceHandle;
__viewConfig: ViewConfig;
constructor(
tag: number,
viewConfig: ViewConfig,
internalInstanceHandle: InternalInstanceHandle
instanceHandle: InstanceHandle,
ownerDocument: ReactNativeDocument
): void;
get offsetHeight(): number;
get offsetLeft(): number;
Expand Down Expand Up @@ -11400,7 +11419,10 @@ declare export function getBoundingClientRect(

exports[`public API should not change unintentionally src/private/webapis/dom/nodes/ReadOnlyNode.js 1`] = `
"declare export default class ReadOnlyNode {
constructor(internalInstanceHandle: InternalInstanceHandle): void;
constructor(
instanceHandle: InstanceHandle,
ownerDocument: ReactNativeDocument | null
): void;
get childNodes(): NodeList<ReadOnlyNode>;
get firstChild(): ReadOnlyNode | null;
get isConnected(): boolean;
Expand All @@ -11409,6 +11431,7 @@ exports[`public API should not change unintentionally src/private/webapis/dom/no
get nodeName(): string;
get nodeType(): number;
get nodeValue(): string | null;
get ownerDocument(): ReactNativeDocument | null;
get parentElement(): ReadOnlyElement | null;
get parentNode(): ReadOnlyNode | null;
get previousSibling(): ReadOnlyNode | null;
Expand Down
Loading

0 comments on commit 8d2c98e

Please sign in to comment.