[RN] Add support for document instance in React Native (#32260)
## Summary We're adding support for `Document` instances in React Native (as `ReactNativeDocument` instances) in https://github.com/facebook/react-native/pull/49012 , which requires the React Fabric renderer to handle its lifecycle. This modifies the renderer to create those document instances and associate them with the React root, and provides a new method for React Native to access them given its containerTag / rootTag. ## How did you test this change? Tested e2e in https://github.com/facebook/react-native/pull/49012 manually syncing these changes.
Rubén Norte committed
Jan 29, 2025 at 17:07 UTC
b2357ecd8203341a3668a96d32d68dd519e5430d
9 files changed
+87
-14
packages/react-native-renderer/src/ReactFabric.js
+28
-2
@@ -41,7 +41,11 @@ import {
41
import {getPublicInstanceFromInternalInstanceHandle} from './ReactFiberConfigFabric';
42
43
// Module provided by RN:
44
-import {ReactFiberErrorDialog} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
44
+import {
45
+ ReactFiberErrorDialog,
46
+ createPublicRootInstance,
47
+ type PublicRootInstance,
48
+} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
49
import {disableLegacyMode} from 'shared/ReactFeatureFlags';
50
51
if (typeof ReactFiberErrorDialog.showErrorDialog !== 'function') {
@@ -126,10 +130,16 @@ function render(
130
onRecoverableError = options.onRecoverableError;
131
}
132
133
+ const publicRootInstance = createPublicRootInstance(containerTag);
134
+ const rootInstance = {
135
+ publicInstance: publicRootInstance,
136
+ containerTag,
137
+ };
138
+
139
// TODO (bvaughn): If we decide to keep the wrapper component,
140
// We could create a wrapper for containerTag as well to reduce special casing.
141
root = createContainer(
132
- containerTag,
142
+ rootInstance,
143
concurrentRoot ? ConcurrentRoot : LegacyRoot,
144
null,
145
false,
@@ -140,6 +150,7 @@ function render(
150
onRecoverableError,
151
null,
152
);
153
+
154
roots.set(containerTag, root);
155
}
156
updateContainer(element, root, null, callback);
@@ -157,6 +168,9 @@ function stopSurface(containerTag: number) {
168
if (root) {
169
// TODO: Is it safe to reset this now or should I wait since this unmount could be deferred?
170
updateContainer(null, root, null, () => {
171
+ // Remove the reference to the public instance to prevent memory leaks.
172
+ root.containerInfo.publicInstance = null;
173
+
174
roots.delete(containerTag);
175
});
176
}
@@ -170,6 +184,16 @@ function createPortal(
184
return createPortalImpl(children, containerTag, null, key);
185
}
186
187
+function getPublicInstanceFromRootTag(
188
+ rootTag: number,
189
+): PublicRootInstance | null {
190
+ const root = roots.get(rootTag);
191
+ if (root) {
192
+ return root.containerInfo.publicInstance;
193
+ }
194
+ return null;
195
+}
196
+
197
setBatchingImplementation(batchedUpdatesImpl, discreteUpdates);
198
199
const roots = new Map<number, FiberRoot>();
@@ -195,6 +219,8 @@ export {
219
// instance handles we use to dispatch events. This provides a way to access
220
// the public instances we created from them (potentially created lazily).
221
getPublicInstanceFromInternalInstanceHandle,
222
+ // Returns the document instance for that root tag.
223
+ getPublicInstanceFromRootTag,
224
// DEV-only:
225
isChildPublicInstance,
226
};
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+10
-5
@@ -31,6 +31,7 @@ import {
31
createPublicTextInstance,
32
type PublicInstance as ReactNativePublicInstance,
33
type PublicTextInstance,
34
+ type PublicRootInstance,
35
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
36
37
const {
@@ -108,7 +109,10 @@ export type TextInstance = {
109
};
110
export type HydratableInstance = Instance | TextInstance;
111
export type PublicInstance = ReactNativePublicInstance;
111
-export type Container = number;
112
+export type Container = {
113
+ containerTag: number,
114
+ publicInstance: PublicRootInstance | null,
115
+};
116
export type ChildSet = Object | Array<Node>;
117
export type HostContext = $ReadOnly<{
118
isInAParentText: boolean,
@@ -180,7 +184,7 @@ export function createInstance(
184
const node = createNode(
185
tag, // reactTag
186
viewConfig.uiViewClassName, // viewName
183
- rootContainerInstance, // rootTag
187
+ rootContainerInstance.containerTag, // rootTag
188
updatePayload, // props
189
internalInstanceHandle, // internalInstanceHandle
190
);
@@ -189,6 +193,7 @@ export function createInstance(
193
tag,
194
viewConfig,
195
internalInstanceHandle,
196
+ rootContainerInstance.publicInstance,
197
);
198
199
return {
@@ -221,7 +226,7 @@ export function createTextInstance(
226
const node = createNode(
227
tag, // reactTag
228
'RCTRawText', // viewName
224
- rootContainerInstance, // rootTag
229
+ rootContainerInstance.containerTag, // rootTag
230
{text: text}, // props
231
internalInstanceHandle, // instance handle
232
);
@@ -501,7 +506,7 @@ export function finalizeContainerChildren(
506
newChildren: ChildSet,
507
): void {
508
if (!enableFabricCompleteRootInCommitPhase) {
504
- completeRoot(container, newChildren);
509
+ completeRoot(container.containerTag, newChildren);
510
}
511
}
512
@@ -511,7 +516,7 @@ export function replaceContainerChildren(
516
): void {
517
// Noop - children will be replaced in finalizeContainerChildren
518
if (enableFabricCompleteRootInCommitPhase) {
514
- completeRoot(container, newChildren);
519
+ completeRoot(container.containerTag, newChildren);
520
}
521
}
522
packages/react-native-renderer/src/ReactFiberConfigNative.js
+9
-5
@@ -15,6 +15,7 @@ import {
15
ReactNativeViewConfigRegistry,
16
UIManager,
17
deepFreezeAndThrowOnMutationInDev,
18
+ type PublicRootInstance,
19
} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
20
21
import {create, diff} from './ReactNativeAttributePayload';
@@ -54,7 +55,10 @@ const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
55
56
export type Type = string;
57
export type Props = Object;
57
-export type Container = number;
58
+export type Container = {
59
+ containerTag: number,
60
+ publicInstance: PublicRootInstance | null,
61
+};
62
export type Instance = ReactNativeFiberHostComponent;
63
export type TextInstance = number;
64
export type HydratableInstance = Instance | TextInstance;
@@ -143,7 +147,7 @@ export function createInstance(
147
UIManager.createView(
148
tag, // reactTag
149
viewConfig.uiViewClassName, // viewName
146
- rootContainerInstance, // rootTag
150
+ rootContainerInstance.containerTag, // rootTag
151
updatePayload, // props
152
);
153
@@ -176,7 +180,7 @@ export function createTextInstance(
180
UIManager.createView(
181
tag, // reactTag
182
'RCTRawText', // viewName
179
- rootContainerInstance, // rootTag
183
+ rootContainerInstance.containerTag, // rootTag
184
{text: text}, // props
185
);
186
@@ -349,7 +353,7 @@ export function appendChildToContainer(
353
): void {
354
const childTag = typeof child === 'number' ? child : child._nativeTag;
355
UIManager.setChildren(
352
- parentInstance, // containerTag
356
+ parentInstance.containerTag, // containerTag
357
[childTag], // reactTags
358
);
359
}
@@ -479,7 +483,7 @@ export function removeChildFromContainer(
483
): void {
484
recursivelyUncacheFiberNode(child);
485
UIManager.manageChildren(
482
- parentInstance, // containerID
486
+ parentInstance.containerTag, // containerID
487
[], // moveFromIndices
488
[], // moveToIndices
489
[], // addChildReactTags
packages/react-native-renderer/src/ReactNativeRenderer.js
+8
-1
@@ -11,6 +11,7 @@ import type {ReactPortal, ReactNodeList} from 'shared/ReactTypes';
11
import type {ElementRef, ElementType, MixedElement} from 'react';
12
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
13
import type {RenderRootOptions} from './ReactNativeTypes';
14
+import type {Container} from 'react-reconciler/src/ReactFiberConfig';
15
16
import './ReactNativeInjection';
17
@@ -143,10 +144,16 @@ function render(
144
onRecoverableError = options.onRecoverableError;
145
}
146
147
+ const rootInstance: Container = {
148
+ containerTag,
149
+ // $FlowExpectedError[incompatible-type] the legacy renderer does not use public root instances
150
+ publicInstance: null,
151
+ };
152
+
153
// TODO (bvaughn): If we decide to keep the wrapper component,
154
// We could create a wrapper for containerTag as well to reduce special casing.
155
root = createContainer(
149
- containerTag,
156
+ rootInstance,
157
LegacyRoot,
158
null,
159
false,
packages/react-native-renderer/src/ReactNativeTypes.js
+1
@@ -231,6 +231,7 @@ export opaque type Node = mixed;
231
export opaque type InternalInstanceHandle = mixed;
232
type PublicInstance = mixed;
233
type PublicTextInstance = mixed;
234
+export opaque type PublicRootInstance = mixed;
235
236
export type ReactFabricType = {
237
findHostInstance_DEPRECATED<TElementType: ElementType>(
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js
+4
@@ -9,6 +9,7 @@
9
10
export opaque type PublicInstance = mixed;
11
export opaque type PublicTextInstance = mixed;
12
+export opaque type PublicRootInstance = mixed;
13
14
module.exports = {
15
get BatchedBridge() {
@@ -59,4 +60,7 @@ module.exports = {
60
get createPublicTextInstance() {
61
return require('./createPublicTextInstance').default;
62
},
63
+ get createPublicRootInstance() {
64
+ return require('./createPublicRootInstance').default;
65
+ },
66
};
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/createPublicInstance.js
+6
-1
@@ -7,15 +7,20 @@
7
* @flow strict
8
*/
9
10
-import type {PublicInstance} from './ReactNativePrivateInterface';
10
+import type {
11
+ PublicInstance,
12
+ PublicRootInstance,
13
+} from './ReactNativePrivateInterface';
14
15
export default function createPublicInstance(
16
tag: number,
17
viewConfig: mixed,
18
internalInstanceHandle: mixed,
19
+ rootPublicInstance: PublicRootInstance | null,
20
): PublicInstance {
21
return {
22
__nativeTag: tag,
23
__internalInstanceHandle: internalInstanceHandle,
24
+ __rootPublicInstance: rootPublicInstance,
25
};
26
}
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/createPublicRootInstance.js
new
+16
@@ -0,0 +1,16 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow strict
8
+ */
9
+
10
+import type {PublicRootInstance} from './ReactNativePrivateInterface';
11
+
12
+export default function createPublicRootInstance(
13
+ rootTag: number,
14
+): PublicRootInstance {
15
+ return null;
16
+}
scripts/flow/react-native-host-hooks.js
+5
@@ -143,6 +143,7 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
143
};
144
declare export opaque type PublicInstance;
145
declare export opaque type PublicTextInstance;
146
+ declare export opaque type PublicRootInstance;
147
declare export function getNodeFromPublicInstance(
148
publicInstance: PublicInstance,
149
): Object;
@@ -153,7 +154,11 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
154
tag: number,
155
viewConfig: __ViewConfig,
156
internalInstanceHandle: mixed,
157
+ publicRootInstance: PublicRootInstance | null,
158
): PublicInstance;
159
+ declare export function createPublicRootInstance(
160
+ rootTag: number,
161
+ ): PublicRootInstance;
162
declare export function createPublicTextInstance(
163
internalInstanceHandle: mixed,
164
): PublicTextInstance;