@samitouri / QOS-React-2 / commits / 0700dd50bd

Implement public instances for text nodes in Fabric (#26516)

## Summary This adds the ability to create public instances for text nodes in Fabric. The implementation for the public instances lives in React Native (as it does for host components after #26437). The logic here just handles their lazy instantiation when requested via `getPublicInstanceFromInternalInstanceHandle`, which is called by Fabric with information coming from the shadow tree. It's important that the creation of public instances for text nodes is done lazily to avoid regressing memory usage when unused. Instances for text nodes are left intact if the public instance is never accessed. This is necessary to implement access to text nodes in React Native as explained in https://github.com/react-native-community/discussions-and-proposals/pull/607 ## How did you test this change? Added unit tests (also fixed a test that was only testing the logic in a mock :S).

Rubén Norte committed Apr 4, 2023 at 15:43 UTC 0700dd50bda98f5ee86f2e3adfe5e9906ed1e8e3
6 files changed +151 -25
packages/react-native-renderer/src/ReactFabricHostConfig.js
+42 -10
@@ -14,13 +14,16 @@ import {
14 DefaultEventPriority,
15 DiscreteEventPriority,
16 } from 'react-reconciler/src/ReactEventPriorities';
17 +import {HostText} from 'react-reconciler/src/ReactWorkTags';
18
19 // Modules provided by RN:
20 import {
21 ReactNativeViewConfigRegistry,
22 deepFreezeAndThrowOnMutationInDev,
23 createPublicInstance,
24 + createPublicTextInstance,
25 type PublicInstance as ReactNativePublicInstance,
26 + type PublicTextInstance,
27 } from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
28
29 const {
@@ -47,23 +50,33 @@ const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
50 // This means that they never overlap.
51 let nextReactTag = 2;
52
53 +type InternalInstanceHandle = Object;
54 type Node = Object;
55 export type Type = string;
56 export type Props = Object;
57 export type Instance = {
58 // Reference to the shadow node.
59 node: Node,
60 + // This object is shared by all the clones of the instance.
61 + // We use it to access their shared public instance (exposed through refs)
62 + // and to access its committed state for events, etc.
63 canonical: {
64 nativeTag: number,
65 viewConfig: ViewConfig,
66 currentProps: Props,
67 // Reference to the React handle (the fiber)
61 - internalInstanceHandle: Object,
68 + internalInstanceHandle: InternalInstanceHandle,
69 // Exposed through refs.
70 publicInstance: PublicInstance,
71 },
72 };
66 -export type TextInstance = {node: Node, ...};
73 +export type TextInstance = {
74 + // Reference to the shadow node.
75 + node: Node,
76 + // Text instances are never cloned, so we don't need to keep a "canonical"
77 + // reference to make sure all clones of the instance point to the same values.
78 + publicInstance?: PublicTextInstance,
79 +};
80 export type HydratableInstance = Instance | TextInstance;
81 export type PublicInstance = ReactNativePublicInstance;
82 export type Container = number;
@@ -115,7 +128,7 @@ export function createInstance(
128 props: Props,
129 rootContainerInstance: Container,
130 hostContext: HostContext,
118 - internalInstanceHandle: Object,
131 + internalInstanceHandle: InternalInstanceHandle,
132 ): Instance {
133 const tag = nextReactTag;
134 nextReactTag += 2;
@@ -162,7 +175,7 @@ export function createTextInstance(
175 text: string,
176 rootContainerInstance: Container,
177 hostContext: HostContext,
165 - internalInstanceHandle: Object,
178 + internalInstanceHandle: InternalInstanceHandle,
179 ): TextInstance {
180 if (__DEV__) {
181 if (!hostContext.isInAParentText) {
@@ -239,9 +252,26 @@ export function getPublicInstance(instance: Instance): null | PublicInstance {
252 return null;
253 }
254
255 +function getPublicTextInstance(
256 + textInstance: TextInstance,
257 + internalInstanceHandle: InternalInstanceHandle,
258 +): PublicTextInstance {
259 + if (textInstance.publicInstance == null) {
260 + textInstance.publicInstance = createPublicTextInstance(
261 + internalInstanceHandle,
262 + );
263 + }
264 + return textInstance.publicInstance;
265 +}
266 +
267 export function getPublicInstanceFromInternalInstanceHandle(
243 - internalInstanceHandle: Object,
244 -): null | PublicInstance {
268 + internalInstanceHandle: InternalInstanceHandle,
269 +): null | PublicInstance | PublicTextInstance {
270 + if (internalInstanceHandle.tag === HostText) {
271 + const textInstance: TextInstance = internalInstanceHandle.stateNode;
272 + return getPublicTextInstance(textInstance, internalInstanceHandle);
273 + }
274 +
275 const instance: Instance = internalInstanceHandle.stateNode;
276 return getPublicInstance(instance);
277 }
@@ -321,7 +351,7 @@ export function cloneInstance(
351 type: string,
352 oldProps: Props,
353 newProps: Props,
324 - internalInstanceHandle: Object,
354 + internalInstanceHandle: InternalInstanceHandle,
355 keepChildren: boolean,
356 recyclableInstance: null | Instance,
357 ): Instance {
@@ -350,7 +380,7 @@ export function cloneHiddenInstance(
380 instance: Instance,
381 type: string,
382 props: Props,
353 - internalInstanceHandle: Object,
383 + internalInstanceHandle: InternalInstanceHandle,
384 ): Instance {
385 const viewConfig = instance.canonical.viewConfig;
386 const node = instance.node;
@@ -367,7 +397,7 @@ export function cloneHiddenInstance(
397 export function cloneHiddenTextInstance(
398 instance: Instance,
399 text: string,
370 - internalInstanceHandle: Object,
400 + internalInstanceHandle: InternalInstanceHandle,
401 ): TextInstance {
402 throw new Error('Not yet implemented.');
403 }
@@ -399,7 +429,9 @@ export function getInstanceFromNode(node: any): empty {
429 throw new Error('Not yet implemented.');
430 }
431
402 -export function beforeActiveInstanceBlur(internalInstanceHandle: Object) {
432 +export function beforeActiveInstanceBlur(
433 + internalInstanceHandle: InternalInstanceHandle,
434 +) {
435 // noop
436 }
437
packages/react-native-renderer/src/ReactNativeTypes.js
+2 -1
@@ -215,6 +215,7 @@ export type ReactNativeType = {
215 export opaque type Node = mixed;
216 export opaque type InternalInstanceHandle = mixed;
217 type PublicInstance = mixed;
218 +type PublicTextInstance = mixed;
219
220 export type ReactFabricType = {
221 findHostInstance_DEPRECATED<TElementType: ElementType>(
@@ -244,7 +245,7 @@ export type ReactFabricType = {
245 ): ?Node,
246 getPublicInstanceFromInternalInstanceHandle(
247 internalInstanceHandle: InternalInstanceHandle,
247 - ): PublicInstance,
248 + ): PublicInstance | PublicTextInstance,
249 ...
250 };
251
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js
+4
@@ -8,6 +8,7 @@
8 */
9
10 export opaque type PublicInstance = mixed;
11 +export opaque type PublicTextInstance = mixed;
12
13 module.exports = {
14 get BatchedBridge() {
@@ -55,4 +56,7 @@ module.exports = {
56 get createPublicInstance() {
57 return require('./createPublicInstance').default;
58 },
59 + get createPublicTextInstance() {
60 + return require('./createPublicTextInstance').default;
61 + },
62 };
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/createPublicTextInstance.js new
+18
@@ -0,0 +1,18 @@
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 {PublicInstance} from './ReactNativePrivateInterface';
11 +
12 +export default function createPublicTextInstance(
13 + internalInstanceHandle: mixed,
14 +): PublicInstance {
15 + return {
16 + __internalInstanceHandle: internalInstanceHandle,
17 + };
18 +}
packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
+81 -14
@@ -12,11 +12,10 @@
12
13 let React;
14 let ReactFabric;
15 +let ReactNativePrivateInterface;
16 let createReactNativeComponentClass;
17 let StrictMode;
18 let act;
18 -let getNativeTagFromPublicInstance;
19 -let getNodeFromPublicInstance;
19
20 const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT =
21 "Warning: dispatchCommand was called with a ref that isn't a " +
@@ -39,14 +38,10 @@ describe('ReactFabric', () => {
38 React = require('react');
39 StrictMode = React.StrictMode;
40 ReactFabric = require('react-native-renderer/fabric');
41 + ReactNativePrivateInterface = require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface');
42 createReactNativeComponentClass =
43 require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface')
44 .ReactNativeViewConfigRegistry.register;
45 - getNativeTagFromPublicInstance =
46 - require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface').getNativeTagFromPublicInstance;
47 - getNodeFromPublicInstance =
48 - require('react-native/Libraries/ReactPrivate/ReactNativePrivateInterface').getNodeFromPublicInstance;
49 -
45 act = require('internal-test-utils').act;
46 });
47
@@ -937,7 +932,9 @@ describe('ReactFabric', () => {
932 '\n in RCTView (at **)' +
933 '\n in ContainsStrictModeChild (at **)',
934 ]);
940 - expect(match).toBe(getNativeTagFromPublicInstance(child));
935 + expect(match).toBe(
936 + ReactNativePrivateInterface.getNativeTagFromPublicInstance(child),
937 + );
938 });
939
940 it('findNodeHandle should warn if passed a component that is inside StrictMode', async () => {
@@ -974,7 +971,9 @@ describe('ReactFabric', () => {
971 '\n in RCTView (at **)' +
972 '\n in IsInStrictMode (at **)',
973 ]);
977 - expect(match).toBe(getNativeTagFromPublicInstance(child));
974 + expect(match).toBe(
975 + ReactNativePrivateInterface.getNativeTagFromPublicInstance(child),
976 + );
977 });
978
979 it('should no-op if calling sendAccessibilityEvent on unmounted refs', async () => {
@@ -1015,6 +1014,30 @@ describe('ReactFabric', () => {
1014 uiViewClassName: 'RCTView',
1015 }));
1016
1017 + await act(() => {
1018 + ReactFabric.render(<View foo="test" />, 1);
1019 + });
1020 +
1021 + const internalInstanceHandle =
1022 + nativeFabricUIManager.createNode.mock.calls[0][4];
1023 + expect(internalInstanceHandle).toEqual(expect.any(Object));
1024 +
1025 + const expectedShadowNode =
1026 + nativeFabricUIManager.createNode.mock.results[0].value;
1027 + expect(expectedShadowNode).toEqual(expect.any(Object));
1028 +
1029 + const node = ReactFabric.getNodeFromInternalInstanceHandle(
1030 + internalInstanceHandle,
1031 + );
1032 + expect(node).toBe(expectedShadowNode);
1033 + });
1034 +
1035 + it('getPublicInstanceFromInternalInstanceHandle should provide public instances for HostComponent', async () => {
1036 + const View = createReactNativeComponentClass('RCTView', () => ({
1037 + validAttributes: {foo: true},
1038 + uiViewClassName: 'RCTView',
1039 + }));
1040 +
1041 let viewRef;
1042 await act(() => {
1043 ReactFabric.render(
@@ -1028,11 +1051,55 @@ describe('ReactFabric', () => {
1051 );
1052 });
1053
1031 - const expectedShadowNode =
1032 - nativeFabricUIManager.createNode.mock.results[0].value;
1033 - expect(expectedShadowNode).toEqual(expect.any(Object));
1054 + const internalInstanceHandle =
1055 + nativeFabricUIManager.createNode.mock.calls[0][4];
1056 + expect(internalInstanceHandle).toEqual(expect.any(Object));
1057
1035 - const node = getNodeFromPublicInstance(viewRef);
1036 - expect(node).toBe(expectedShadowNode);
1058 + const publicInstance =
1059 + ReactFabric.getPublicInstanceFromInternalInstanceHandle(
1060 + internalInstanceHandle,
1061 + );
1062 + expect(publicInstance).toBe(viewRef);
1063 + });
1064 +
1065 + it('getPublicInstanceFromInternalInstanceHandle should provide public instances for HostText', async () => {
1066 + jest.spyOn(ReactNativePrivateInterface, 'createPublicTextInstance');
1067 +
1068 + const RCTText = createReactNativeComponentClass('RCTText', () => ({
1069 + validAttributes: {},
1070 + uiViewClassName: 'RCTText',
1071 + }));
1072 +
1073 + await act(() => {
1074 + ReactFabric.render(<RCTText>Text content</RCTText>, 1);
1075 + });
1076 +
1077 + // Access the internal instance handle used to create the text node.
1078 + const internalInstanceHandle =
1079 + nativeFabricUIManager.createNode.mock.calls[0][4];
1080 + expect(internalInstanceHandle).toEqual(expect.any(Object));
1081 +
1082 + // Text public instances should be created lazily.
1083 + expect(
1084 + ReactNativePrivateInterface.createPublicTextInstance,
1085 + ).not.toHaveBeenCalled();
1086 +
1087 + const publicInstance =
1088 + ReactFabric.getPublicInstanceFromInternalInstanceHandle(
1089 + internalInstanceHandle,
1090 + );
1091 +
1092 + // We just requested the text public instance, so it should have been created at this point.
1093 + expect(
1094 + ReactNativePrivateInterface.createPublicTextInstance,
1095 + ).toHaveBeenCalledTimes(1);
1096 + expect(
1097 + ReactNativePrivateInterface.createPublicTextInstance,
1098 + ).toHaveBeenCalledWith(internalInstanceHandle);
1099 +
1100 + const expectedPublicInstance =
1101 + ReactNativePrivateInterface.createPublicTextInstance.mock.results[0]
1102 + .value;
1103 + expect(publicInstance).toBe(expectedPublicInstance);
1104 });
1105 });
scripts/flow/react-native-host-hooks.js
+4
@@ -145,6 +145,7 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
145 ...
146 };
147 declare export opaque type PublicInstance;
148 + declare export opaque type PublicTextInstance;
149 declare export function getNodeFromPublicInstance(
150 publicInstance: PublicInstance,
151 ): Object;
@@ -156,6 +157,9 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
157 viewConfig: __ViewConfig,
158 internalInstanceHandle: mixed,
159 ): PublicInstance;
160 + declare export function createPublicTextInstance(
161 + internalInstanceHandle: mixed,
162 + ): PublicTextInstance;
163 }
164
165 declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore' {