@samitouri / QOS-React-2 / commits / 7a3ffef703

[react-native] Consume ReactNativeAttributePayloadFabric from ReactNativePrivateInterface (#33616)

## Summary ReactNativeAttributePayloadFabric was synced to react-native in https://github.com/facebook/react-native/commit/0e42d33cbcfadcf5d787108da785d56a83d07a9f. We should now consume these methods from the ReactNativePrivateInterface. Moving these methods to the React Native repo gives us more flexibility to experiment with new techniques for bridging and diffing props payloads. I did have to leave some stub implementations for existing unit tests, but moved all detailed tests to the React Native repo. ## How did you test this change? * `yarn prettier` * `yarn test ReactFabric-test`

Pieter De Baets committed Jun 25, 2025 at 10:23 UTC 7a3ffef70339c10f8d65a27b88cd73bfbe13eb8a
9 files changed +82 -1014
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+12 -4
@@ -12,7 +12,6 @@ import type {
12 TouchedViewDataAtPoint,
13 ViewConfig,
14 } from './ReactNativeTypes';
15 -import {create, diff} from './ReactNativeAttributePayloadFabric';
15 import {dispatchEvent} from './ReactFabricEventEmitter';
16 import {
17 NoEventPriority,
@@ -35,6 +34,8 @@ import {
34 deepFreezeAndThrowOnMutationInDev,
35 createPublicInstance,
36 createPublicTextInstance,
37 + createAttributePayload,
38 + diffAttributePayloads,
39 type PublicInstance as ReactNativePublicInstance,
40 type PublicTextInstance,
41 type PublicRootInstance,
@@ -190,7 +191,10 @@ export function createInstance(
191 }
192 }
193
193 - const updatePayload = create(props, viewConfig.validAttributes);
194 + const updatePayload = createAttributePayload(
195 + props,
196 + viewConfig.validAttributes,
197 + );
198
199 const node = createNode(
200 tag, // reactTag
@@ -456,7 +460,11 @@ export function cloneInstance(
460 newChildSet: ?ChildSet,
461 ): Instance {
462 const viewConfig = instance.canonical.viewConfig;
459 - const updatePayload = diff(oldProps, newProps, viewConfig.validAttributes);
463 + const updatePayload = diffAttributePayloads(
464 + oldProps,
465 + newProps,
466 + viewConfig.validAttributes,
467 + );
468 // TODO: If the event handlers have changed, we need to update the current props
469 // in the commit phase but there is no host config hook to do it yet.
470 // So instead we hack it by updating it in the render phase.
@@ -505,7 +513,7 @@ export function cloneHiddenInstance(
513 ): Instance {
514 const viewConfig = instance.canonical.viewConfig;
515 const node = instance.node;
508 - const updatePayload = create(
516 + const updatePayload = createAttributePayload(
517 {style: {display: 'none'}},
518 viewConfig.validAttributes,
519 );
packages/react-native-renderer/src/ReactNativeAttributePayloadFabric.js deleted
-514
@@ -1,514 +0,0 @@
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
8 - */
9 -
10 -// Modules provided by RN:
11 -import {
12 - deepDiffer,
13 - flattenStyle,
14 -} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
15 -import isArray from 'shared/isArray';
16 -
17 -import type {AttributeConfiguration} from './ReactNativeTypes';
18 -
19 -const emptyObject = {};
20 -
21 -/**
22 - * Create a payload that contains all the updates between two sets of props.
23 - *
24 - * These helpers are all encapsulated into a single module, because they use
25 - * mutation as a performance optimization which leads to subtle shared
26 - * dependencies between the code paths. To avoid this mutable state leaking
27 - * across modules, I've kept them isolated to this module.
28 - */
29 -
30 -type NestedNode = Array<NestedNode> | Object;
31 -
32 -// Tracks removed keys
33 -let removedKeys: {[string]: boolean} | null = null;
34 -let removedKeyCount = 0;
35 -
36 -const deepDifferOptions = {
37 - unsafelyIgnoreFunctions: true,
38 -};
39 -
40 -function defaultDiffer(prevProp: mixed, nextProp: mixed): boolean {
41 - if (typeof nextProp !== 'object' || nextProp === null) {
42 - // Scalars have already been checked for equality
43 - return true;
44 - } else {
45 - // For objects and arrays, the default diffing algorithm is a deep compare
46 - return deepDiffer(prevProp, nextProp, deepDifferOptions);
47 - }
48 -}
49 -
50 -function restoreDeletedValuesInNestedArray(
51 - updatePayload: Object,
52 - node: NestedNode,
53 - validAttributes: AttributeConfiguration,
54 -) {
55 - if (isArray(node)) {
56 - let i = node.length;
57 - while (i-- && removedKeyCount > 0) {
58 - restoreDeletedValuesInNestedArray(
59 - updatePayload,
60 - node[i],
61 - validAttributes,
62 - );
63 - }
64 - } else if (node && removedKeyCount > 0) {
65 - const obj = node;
66 - for (const propKey in removedKeys) {
67 - // $FlowFixMe[incompatible-use] found when upgrading Flow
68 - if (!removedKeys[propKey]) {
69 - continue;
70 - }
71 - let nextProp = obj[propKey];
72 - if (nextProp === undefined) {
73 - continue;
74 - }
75 -
76 - const attributeConfig = validAttributes[propKey];
77 - if (!attributeConfig) {
78 - continue; // not a valid native prop
79 - }
80 -
81 - if (typeof nextProp === 'function') {
82 - // $FlowFixMe[incompatible-type] found when upgrading Flow
83 - nextProp = true;
84 - }
85 - if (typeof nextProp === 'undefined') {
86 - // $FlowFixMe[incompatible-type] found when upgrading Flow
87 - nextProp = null;
88 - }
89 -
90 - if (typeof attributeConfig !== 'object') {
91 - // case: !Object is the default case
92 - updatePayload[propKey] = nextProp;
93 - } else if (
94 - typeof attributeConfig.diff === 'function' ||
95 - typeof attributeConfig.process === 'function'
96 - ) {
97 - // case: CustomAttributeConfiguration
98 - const nextValue =
99 - typeof attributeConfig.process === 'function'
100 - ? attributeConfig.process(nextProp)
101 - : nextProp;
102 - updatePayload[propKey] = nextValue;
103 - }
104 - // $FlowFixMe[incompatible-use] found when upgrading Flow
105 - removedKeys[propKey] = false;
106 - removedKeyCount--;
107 - }
108 - }
109 -}
110 -
111 -function diffNestedArrayProperty(
112 - updatePayload: null | Object,
113 - prevArray: Array<NestedNode>,
114 - nextArray: Array<NestedNode>,
115 - validAttributes: AttributeConfiguration,
116 -): null | Object {
117 - const minLength =
118 - prevArray.length < nextArray.length ? prevArray.length : nextArray.length;
119 - let i;
120 - for (i = 0; i < minLength; i++) {
121 - // Diff any items in the array in the forward direction. Repeated keys
122 - // will be overwritten by later values.
123 - updatePayload = diffNestedProperty(
124 - updatePayload,
125 - prevArray[i],
126 - nextArray[i],
127 - validAttributes,
128 - );
129 - }
130 - for (; i < prevArray.length; i++) {
131 - // Clear out all remaining properties.
132 - updatePayload = clearNestedProperty(
133 - updatePayload,
134 - prevArray[i],
135 - validAttributes,
136 - );
137 - }
138 - for (; i < nextArray.length; i++) {
139 - // Add all remaining properties
140 - const nextProp = nextArray[i];
141 - if (!nextProp) {
142 - continue;
143 - }
144 - updatePayload = addNestedProperty(updatePayload, nextProp, validAttributes);
145 - }
146 - return updatePayload;
147 -}
148 -
149 -function diffNestedProperty(
150 - updatePayload: null | Object,
151 - prevProp: NestedNode,
152 - nextProp: NestedNode,
153 - validAttributes: AttributeConfiguration,
154 -): null | Object {
155 - if (!updatePayload && prevProp === nextProp) {
156 - // If no properties have been added, then we can bail out quickly on object
157 - // equality.
158 - return updatePayload;
159 - }
160 -
161 - if (!prevProp || !nextProp) {
162 - if (nextProp) {
163 - return addNestedProperty(updatePayload, nextProp, validAttributes);
164 - }
165 - if (prevProp) {
166 - return clearNestedProperty(updatePayload, prevProp, validAttributes);
167 - }
168 - return updatePayload;
169 - }
170 -
171 - if (!isArray(prevProp) && !isArray(nextProp)) {
172 - // Both are leaves, we can diff the leaves.
173 - return diffProperties(updatePayload, prevProp, nextProp, validAttributes);
174 - }
175 -
176 - if (isArray(prevProp) && isArray(nextProp)) {
177 - // Both are arrays, we can diff the arrays.
178 - return diffNestedArrayProperty(
179 - updatePayload,
180 - prevProp,
181 - nextProp,
182 - validAttributes,
183 - );
184 - }
185 -
186 - if (isArray(prevProp)) {
187 - return diffProperties(
188 - updatePayload,
189 - flattenStyle(prevProp),
190 - nextProp,
191 - validAttributes,
192 - );
193 - }
194 -
195 - return diffProperties(
196 - updatePayload,
197 - prevProp,
198 - flattenStyle(nextProp),
199 - validAttributes,
200 - );
201 -}
202 -
203 -/**
204 - * clearNestedProperty takes a single set of props and valid attributes. It
205 - * adds a null sentinel to the updatePayload, for each prop key.
206 - */
207 -function clearNestedProperty(
208 - updatePayload: null | Object,
209 - prevProp: NestedNode,
210 - validAttributes: AttributeConfiguration,
211 -): null | Object {
212 - if (!prevProp) {
213 - return updatePayload;
214 - }
215 -
216 - if (!isArray(prevProp)) {
217 - // Add each property of the leaf.
218 - return clearProperties(updatePayload, prevProp, validAttributes);
219 - }
220 -
221 - for (let i = 0; i < prevProp.length; i++) {
222 - // Add all the properties of the array.
223 - updatePayload = clearNestedProperty(
224 - updatePayload,
225 - prevProp[i],
226 - validAttributes,
227 - );
228 - }
229 - return updatePayload;
230 -}
231 -
232 -/**
233 - * diffProperties takes two sets of props and a set of valid attributes
234 - * and write to updatePayload the values that changed or were deleted.
235 - * If no updatePayload is provided, a new one is created and returned if
236 - * anything changed.
237 - */
238 -function diffProperties(
239 - updatePayload: null | Object,
240 - prevProps: Object,
241 - nextProps: Object,
242 - validAttributes: AttributeConfiguration,
243 -): null | Object {
244 - let attributeConfig;
245 - let nextProp;
246 - let prevProp;
247 -
248 - for (const propKey in nextProps) {
249 - attributeConfig = validAttributes[propKey];
250 - if (!attributeConfig) {
251 - continue; // not a valid native prop
252 - }
253 -
254 - prevProp = prevProps[propKey];
255 - nextProp = nextProps[propKey];
256 -
257 - if (typeof nextProp === 'function') {
258 - const attributeConfigHasProcess =
259 - typeof attributeConfig === 'object' &&
260 - typeof attributeConfig.process === 'function';
261 - if (!attributeConfigHasProcess) {
262 - // functions are converted to booleans as markers that the associated
263 - // events should be sent from native.
264 - nextProp = (true: any);
265 - // If nextProp is not a function, then don't bother changing prevProp
266 - // since nextProp will win and go into the updatePayload regardless.
267 - if (typeof prevProp === 'function') {
268 - prevProp = (true: any);
269 - }
270 - }
271 - }
272 -
273 - // An explicit value of undefined is treated as a null because it overrides
274 - // any other preceding value.
275 - if (typeof nextProp === 'undefined') {
276 - nextProp = (null: any);
277 - if (typeof prevProp === 'undefined') {
278 - prevProp = (null: any);
279 - }
280 - }
281 -
282 - if (removedKeys) {
283 - removedKeys[propKey] = false;
284 - }
285 -
286 - if (updatePayload && updatePayload[propKey] !== undefined) {
287 - // Something else already triggered an update to this key because another
288 - // value diffed. Since we're now later in the nested arrays our value is
289 - // more important so we need to calculate it and override the existing
290 - // value. It doesn't matter if nothing changed, we'll set it anyway.
291 -
292 - // Pattern match on: attributeConfig
293 - if (typeof attributeConfig !== 'object') {
294 - // case: !Object is the default case
295 - updatePayload[propKey] = nextProp;
296 - } else if (
297 - typeof attributeConfig.diff === 'function' ||
298 - typeof attributeConfig.process === 'function'
299 - ) {
300 - // case: CustomAttributeConfiguration
301 - const nextValue =
302 - typeof attributeConfig.process === 'function'
303 - ? attributeConfig.process(nextProp)
304 - : nextProp;
305 - updatePayload[propKey] = nextValue;
306 - }
307 - continue;
308 - }
309 -
310 - if (prevProp === nextProp) {
311 - continue; // nothing changed
312 - }
313 -
314 - // Pattern match on: attributeConfig
315 - if (typeof attributeConfig !== 'object') {
316 - // case: !Object is the default case
317 - if (defaultDiffer(prevProp, nextProp)) {
318 - // a normal leaf has changed
319 - (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
320 - propKey
321 - ] = nextProp;
322 - }
323 - } else if (
324 - typeof attributeConfig.diff === 'function' ||
325 - typeof attributeConfig.process === 'function'
326 - ) {
327 - // case: CustomAttributeConfiguration
328 - const shouldUpdate =
329 - prevProp === undefined ||
330 - (typeof attributeConfig.diff === 'function'
331 - ? attributeConfig.diff(prevProp, nextProp)
332 - : defaultDiffer(prevProp, nextProp));
333 - if (shouldUpdate) {
334 - const nextValue =
335 - typeof attributeConfig.process === 'function'
336 - ? // $FlowFixMe[incompatible-use] found when upgrading Flow
337 - attributeConfig.process(nextProp)
338 - : nextProp;
339 - (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
340 - propKey
341 - ] = nextValue;
342 - }
343 - } else {
344 - // default: fallthrough case when nested properties are defined
345 - removedKeys = null;
346 - removedKeyCount = 0;
347 - // We think that attributeConfig is not CustomAttributeConfiguration at
348 - // this point so we assume it must be AttributeConfiguration.
349 - updatePayload = diffNestedProperty(
350 - updatePayload,
351 - prevProp,
352 - nextProp,
353 - ((attributeConfig: any): AttributeConfiguration),
354 - );
355 - if (removedKeyCount > 0 && updatePayload) {
356 - restoreDeletedValuesInNestedArray(
357 - updatePayload,
358 - nextProp,
359 - ((attributeConfig: any): AttributeConfiguration),
360 - );
361 - removedKeys = null;
362 - }
363 - }
364 - }
365 -
366 - // Also iterate through all the previous props to catch any that have been
367 - // removed and make sure native gets the signal so it can reset them to the
368 - // default.
369 - for (const propKey in prevProps) {
370 - if (nextProps[propKey] !== undefined) {
371 - continue; // we've already covered this key in the previous pass
372 - }
373 - attributeConfig = validAttributes[propKey];
374 - if (!attributeConfig) {
375 - continue; // not a valid native prop
376 - }
377 -
378 - if (updatePayload && updatePayload[propKey] !== undefined) {
379 - // This was already updated to a diff result earlier.
380 - continue;
381 - }
382 -
383 - prevProp = prevProps[propKey];
384 - if (prevProp === undefined) {
385 - continue; // was already empty anyway
386 - }
387 - // Pattern match on: attributeConfig
388 - if (
389 - typeof attributeConfig !== 'object' ||
390 - typeof attributeConfig.diff === 'function' ||
391 - typeof attributeConfig.process === 'function'
392 - ) {
393 - // case: CustomAttributeConfiguration | !Object
394 - // Flag the leaf property for removal by sending a sentinel.
395 - (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[
396 - propKey
397 - ] = null;
398 - if (!removedKeys) {
399 - removedKeys = ({}: {[string]: boolean});
400 - }
401 - if (!removedKeys[propKey]) {
402 - removedKeys[propKey] = true;
403 - removedKeyCount++;
404 - }
405 - } else {
406 - // default:
407 - // This is a nested attribute configuration where all the properties
408 - // were removed so we need to go through and clear out all of them.
409 - updatePayload = clearNestedProperty(
410 - updatePayload,
411 - prevProp,
412 - ((attributeConfig: any): AttributeConfiguration),
413 - );
414 - }
415 - }
416 - return updatePayload;
417 -}
418 -
419 -function addNestedProperty(
420 - payload: null | Object,
421 - props: Object,
422 - validAttributes: AttributeConfiguration,
423 -): null | Object {
424 - // Flatten nested style props.
425 - if (isArray(props)) {
426 - for (let i = 0; i < props.length; i++) {
427 - payload = addNestedProperty(payload, props[i], validAttributes);
428 - }
429 - return payload;
430 - }
431 -
432 - for (const propKey in props) {
433 - const prop = props[propKey];
434 -
435 - const attributeConfig = ((validAttributes[
436 - propKey
437 - ]: any): AttributeConfiguration);
438 -
439 - if (attributeConfig == null) {
440 - continue;
441 - }
442 -
443 - let newValue;
444 -
445 - if (prop === undefined) {
446 - // Discard the prop if it was previously defined.
447 - if (payload && payload[propKey] !== undefined) {
448 - newValue = null;
449 - } else {
450 - continue;
451 - }
452 - } else if (typeof attributeConfig === 'object') {
453 - if (typeof attributeConfig.process === 'function') {
454 - // An atomic prop with custom processing.
455 - newValue = attributeConfig.process(prop);
456 - } else if (typeof attributeConfig.diff === 'function') {
457 - // An atomic prop with custom diffing. We don't need to do diffing when adding props.
458 - newValue = prop;
459 - }
460 - } else {
461 - if (typeof prop === 'function') {
462 - // A function prop. It represents an event handler. Pass it to native as 'true'.
463 - newValue = true;
464 - } else {
465 - // An atomic prop. Doesn't need to be flattened.
466 - newValue = prop;
467 - }
468 - }
469 -
470 - if (newValue !== undefined) {
471 - if (!payload) {
472 - payload = ({}: {[string]: $FlowFixMe});
473 - }
474 - payload[propKey] = newValue;
475 - continue;
476 - }
477 -
478 - payload = addNestedProperty(payload, prop, attributeConfig);
479 - }
480 -
481 - return payload;
482 -}
483 -
484 -/**
485 - * clearProperties clears all the previous props by adding a null sentinel
486 - * to the payload for each valid key.
487 - */
488 -function clearProperties(
489 - updatePayload: null | Object,
490 - prevProps: Object,
491 - validAttributes: AttributeConfiguration,
492 -): null | Object {
493 - return diffProperties(updatePayload, prevProps, emptyObject, validAttributes);
494 -}
495 -
496 -export function create(
497 - props: Object,
498 - validAttributes: AttributeConfiguration,
499 -): null | Object {
500 - return addNestedProperty(null, props, validAttributes);
501 -}
502 -
503 -export function diff(
504 - prevProps: Object,
505 - nextProps: Object,
506 - validAttributes: AttributeConfiguration,
507 -): null | Object {
508 - return diffProperties(
509 - null, // updatePayload
510 - prevProps,
511 - nextProps,
512 - validAttributes,
513 - );
514 -}
packages/react-native-renderer/src/ReactNativeTypes.js
+1 -10
@@ -34,15 +34,6 @@ export type AttributeType<T, V> =
34 export type AnyAttributeType = AttributeType<$FlowFixMe, $FlowFixMe>;
35
36 export type AttributeConfiguration = $ReadOnly<{
37 - [propName: string]: AnyAttributeType,
38 - style: $ReadOnly<{
39 - [propName: string]: AnyAttributeType,
40 - ...
41 - }>,
42 - ...
43 -}>;
44 -
45 -export type PartialAttributeConfiguration = $ReadOnly<{
37 [propName: string]: AnyAttributeType,
38 style?: $ReadOnly<{
39 [propName: string]: AnyAttributeType,
@@ -83,7 +74,7 @@ export type PartialViewConfig = $ReadOnly<{
74 directEventTypes?: ViewConfig['directEventTypes'],
75 supportsRawText?: boolean,
76 uiViewClassName: string,
86 - validAttributes?: PartialAttributeConfiguration,
77 + validAttributes?: AttributeConfiguration,
78 }>;
79
80 type InspectorDataProps = $ReadOnly<{
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js
+6
@@ -59,4 +59,10 @@ module.exports = {
59 get createPublicRootInstance() {
60 return require('./createPublicRootInstance').default;
61 },
62 + get createAttributePayload() {
63 + return require('./createAttributePayload').default;
64 + },
65 + get diffAttributePayloads() {
66 + return require('./diffAttributePayloads').default;
67 + },
68 };
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/createAttributePayload.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 {AttributeConfiguration} from '../../../../ReactNativeTypes';
11 +
12 +export default function create(
13 + props: Object,
14 + validAttributes: AttributeConfiguration,
15 +): null | Object {
16 + const {children, ...propsToPass} = props;
17 + return propsToPass;
18 +}
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/diffAttributePayloads.js new
+22
@@ -0,0 +1,22 @@
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 {AttributeConfiguration} from '../../../../ReactNativeTypes';
11 +
12 +import deepDiffer from './deepDiffer';
13 +
14 +export default function diff(
15 + prevProps: Object,
16 + nextProps: Object,
17 + validAttributes: AttributeConfiguration,
18 +): null | Object {
19 + const {children: _prevChildren, ...prevPropsPassed} = prevProps;
20 + const {children: _nextChildren, ...nextPropsToPass} = nextProps;
21 + return deepDiffer(prevPropsPassed, nextPropsToPass) ? nextPropsToPass : null;
22 +}
packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
+13 -6
@@ -184,6 +184,10 @@ describe('ReactFabric', () => {
184 nativeFabricUIManager.cloneNodeWithNewChildrenAndProps,
185 ).not.toBeCalled();
186
187 + jest
188 + .spyOn(ReactNativePrivateInterface, 'diffAttributePayloads')
189 + .mockReturnValue({bar: 'b'});
190 +
191 await act(() => {
192 ReactFabric.render(
193 <Text foo="a" bar="b">
@@ -203,6 +207,9 @@ describe('ReactFabric', () => {
207 RCTText {"foo":"a","bar":"b"}
208 RCTRawText {"text":"1"}`);
209
210 + jest
211 + .spyOn(ReactNativePrivateInterface, 'diffAttributePayloads')
212 + .mockReturnValue({foo: 'b'});
213 await act(() => {
214 ReactFabric.render(
215 <Text foo="b" bar="b">
@@ -612,7 +619,7 @@ describe('ReactFabric', () => {
619 ReactFabric.render(<Component chars={before} />, 11, null, true);
620 });
621 expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
615 - RCTView null
622 + RCTView {}
623 RCTView {"title":"a"}
624 RCTView {"title":"b"}
625 RCTView {"title":"c"}
@@ -638,7 +645,7 @@ describe('ReactFabric', () => {
645 ReactFabric.render(<Component chars={after} />, 11, null, true);
646 });
647 expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
641 - RCTView null
648 + RCTView {}
649 RCTView {"title":"m"}
650 RCTView {"title":"x"}
651 RCTView {"title":"h"}
@@ -700,8 +707,8 @@ describe('ReactFabric', () => {
707 });
708 expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(
709 `11
703 - RCTView null
704 - RCTView null
710 + RCTView {}
711 + RCTView {}
712 RCTView {"title":"a"}
713 RCTView {"title":"b"}
714 RCTView {"title":"c"}
@@ -732,8 +739,8 @@ describe('ReactFabric', () => {
739 });
740 });
741 expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11
735 - RCTView null
736 - RCTView null
742 + RCTView {}
743 + RCTView {}
744 RCTView {"title":"m"}
745 RCTView {"title":"x"}
746 RCTView {"title":"h"}
packages/react-native-renderer/src/__tests__/ReactNativeAttributePayloadFabric-test.internal.js deleted
-480
@@ -1,480 +0,0 @@
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 - * @jest-environment node
8 - */
9 -'use strict';
10 -
11 -const {diff, create} = require('../ReactNativeAttributePayloadFabric');
12 -
13 -describe('ReactNativeAttributePayloadFabric.create', () => {
14 - it('should work with simple example', () => {
15 - expect(create({b: 2, c: 3}, {a: true, b: true})).toEqual({
16 - b: 2,
17 - });
18 - });
19 -
20 - it('should work with complex example', () => {
21 - const validAttributes = {
22 - style: {
23 - position: true,
24 - zIndex: true,
25 - flexGrow: true,
26 - flexShrink: true,
27 - flexDirection: true,
28 - overflow: true,
29 - backgroundColor: true,
30 - },
31 - };
32 -
33 - expect(
34 - create(
35 - {
36 - style: [
37 - {
38 - flexGrow: 1,
39 - flexShrink: 1,
40 - flexDirection: 'row',
41 - overflow: 'scroll',
42 - },
43 - [
44 - {position: 'relative', zIndex: 2},
45 - {flexGrow: 0},
46 - {backgroundColor: 'red'},
47 - ],
48 - ],
49 - },
50 - validAttributes,
51 - ),
52 - ).toEqual({
53 - flexGrow: 0,
54 - flexShrink: 1,
55 - flexDirection: 'row',
56 - overflow: 'scroll',
57 - position: 'relative',
58 - zIndex: 2,
59 - backgroundColor: 'red',
60 - });
61 - });
62 -
63 - it('should nullify previously defined style prop that is subsequently set to null or undefined', () => {
64 - expect(
65 - create({style: [{a: 0}, {a: undefined}]}, {style: {a: true}}),
66 - ).toEqual({a: null});
67 - expect(create({style: [{a: 0}, {a: null}]}, {style: {a: true}})).toEqual({
68 - a: null,
69 - });
70 - });
71 -
72 - it('should ignore non-style fields that are set to undefined', () => {
73 - expect(create({}, {a: true})).toEqual(null);
74 - expect(create({a: undefined}, {a: true})).toEqual(null);
75 - expect(create({a: undefined, b: undefined}, {a: true, b: true})).toEqual(
76 - null,
77 - );
78 - expect(
79 - create({a: undefined, b: undefined, c: 1}, {a: true, b: true}),
80 - ).toEqual(null);
81 - expect(
82 - create({a: undefined, b: undefined, c: 1}, {a: true, b: true, c: true}),
83 - ).toEqual({c: 1});
84 - expect(
85 - create({a: 1, b: undefined, c: 2}, {a: true, b: true, c: true}),
86 - ).toEqual({a: 1, c: 2});
87 - });
88 -
89 - it('should ignore invalid fields', () => {
90 - expect(create({b: 2}, {})).toEqual(null);
91 - });
92 -
93 - it('should not use the diff attribute', () => {
94 - const diffA = jest.fn();
95 - expect(create({a: [2]}, {a: {diff: diffA}})).toEqual({a: [2]});
96 - expect(diffA).not.toBeCalled();
97 - });
98 -
99 - it('should use the process attribute', () => {
100 - const processA = jest.fn(a => a + 1);
101 - expect(create({a: 2}, {a: {process: processA}})).toEqual({a: 3});
102 - expect(processA).toBeCalledWith(2);
103 - });
104 -
105 - it('should use the process attribute for functions as well', () => {
106 - const process = x => x;
107 - const nextFunction = () => {};
108 - expect(create({a: nextFunction}, {a: {process}})).toEqual({
109 - a: nextFunction,
110 - });
111 - });
112 -
113 - it('should work with undefined styles', () => {
114 - expect(create({style: undefined}, {style: {b: true}})).toEqual(null);
115 - expect(create({style: {a: '#ffffff', b: 1}}, {style: {b: true}})).toEqual({
116 - b: 1,
117 - });
118 - });
119 -
120 - it('should flatten nested styles and predefined styles', () => {
121 - const validStyleAttribute = {someStyle: {foo: true, bar: true}};
122 - expect(
123 - create({someStyle: [{foo: 1}, {bar: 2}]}, validStyleAttribute),
124 - ).toEqual({foo: 1, bar: 2});
125 - expect(create({}, validStyleAttribute)).toEqual(null);
126 - const barStyle = {
127 - bar: 3,
128 - };
129 - expect(
130 - create(
131 - {someStyle: [[{foo: 1}, {foo: 2}], barStyle]},
132 - validStyleAttribute,
133 - ),
134 - ).toEqual({foo: 2, bar: 3});
135 - });
136 -
137 - it('should not flatten nested props if attribute config is a primitive or only has diff/process', () => {
138 - expect(create({a: {foo: 1, bar: 2}}, {a: true})).toEqual({
139 - a: {foo: 1, bar: 2},
140 - });
141 - expect(create({a: [{foo: 1}, {bar: 2}]}, {a: true})).toEqual({
142 - a: [{foo: 1}, {bar: 2}],
143 - });
144 - expect(create({a: {foo: 1, bar: 2}}, {a: {diff: a => a}})).toEqual({
145 - a: {foo: 1, bar: 2},
146 - });
147 - expect(
148 - create({a: [{foo: 1}, {bar: 2}]}, {a: {diff: a => a, process: a => a}}),
149 - ).toEqual({a: [{foo: 1}, {bar: 2}]});
150 - });
151 -
152 - it('handles attributes defined multiple times', () => {
153 - const validAttributes = {foo: true, style: {foo: true}};
154 - expect(create({foo: 4, style: {foo: 2}}, validAttributes)).toEqual({
155 - foo: 2,
156 - });
157 - expect(create({style: {foo: 2}}, validAttributes)).toEqual({
158 - foo: 2,
159 - });
160 - expect(create({style: {foo: 2}, foo: 4}, validAttributes)).toEqual({
161 - foo: 4,
162 - });
163 - expect(create({foo: 4, style: {foo: null}}, validAttributes)).toEqual({
164 - foo: null, // this should ideally be null.
165 - });
166 - expect(
167 - create({foo: 4, style: [{foo: null}, {foo: 5}]}, validAttributes),
168 - ).toEqual({
169 - foo: 5,
170 - });
171 - });
172 -
173 - // Function properties are just markers to native that events should be sent.
174 - it('should convert functions to booleans', () => {
175 - expect(
176 - create(
177 - {
178 - a: function () {
179 - return 9;
180 - },
181 - b: function () {
182 - return 3;
183 - },
184 - },
185 - {a: true, b: true},
186 - ),
187 - ).toEqual({a: true, b: true});
188 - });
189 -});
190 -
191 -describe('ReactNativeAttributePayloadFabric.diff', () => {
192 - it('should work with simple example', () => {
193 - expect(diff({a: 1, c: 3}, {b: 2, c: 3}, {a: true, b: true})).toEqual({
194 - a: null,
195 - b: 2,
196 - });
197 - });
198 -
199 - it('should skip fields that are equal', () => {
200 - expect(
201 - diff(
202 - {a: 1, b: 'two', c: true, d: false, e: undefined, f: 0},
203 - {a: 1, b: 'two', c: true, d: false, e: undefined, f: 0},
204 - {a: true, b: true, c: true, d: true, e: true, f: true},
205 - ),
206 - ).toEqual(null);
207 - });
208 -
209 - it('should remove fields', () => {
210 - expect(diff({a: 1}, {}, {a: true})).toEqual({a: null});
211 - });
212 -
213 - it('should remove fields that are set to undefined', () => {
214 - expect(diff({a: 1}, {a: undefined}, {a: true})).toEqual({a: null});
215 - });
216 -
217 - it('should ignore invalid fields', () => {
218 - expect(diff({a: 1}, {b: 2}, {})).toEqual(null);
219 - });
220 -
221 - it('should use the diff attribute', () => {
222 - const diffA = jest.fn((a, b) => true);
223 - const diffB = jest.fn((a, b) => false);
224 - expect(
225 - diff(
226 - {a: [1], b: [3]},
227 - {a: [2], b: [4]},
228 - {a: {diff: diffA}, b: {diff: diffB}},
229 - ),
230 - ).toEqual({a: [2]});
231 - expect(diffA).toBeCalledWith([1], [2]);
232 - expect(diffB).toBeCalledWith([3], [4]);
233 - });
234 -
235 - it('should not use the diff attribute on addition/removal', () => {
236 - const diffA = jest.fn();
237 - const diffB = jest.fn();
238 - expect(
239 - diff({a: [1]}, {b: [2]}, {a: {diff: diffA}, b: {diff: diffB}}),
240 - ).toEqual({a: null, b: [2]});
241 - expect(diffA).not.toBeCalled();
242 - expect(diffB).not.toBeCalled();
243 - });
244 -
245 - it('should do deep diffs of Objects by default', () => {
246 - expect(
247 - diff(
248 - {a: [1], b: {k: [3, 4]}, c: {k: [4, 4]}},
249 - {a: [2], b: {k: [3, 4]}, c: {k: [4, 5]}},
250 - {a: true, b: true, c: true},
251 - ),
252 - ).toEqual({a: [2], c: {k: [4, 5]}});
253 - });
254 -
255 - it('should work with undefined styles', () => {
256 - expect(
257 - diff(
258 - {style: {a: '#ffffff', b: 1}},
259 - {style: undefined},
260 - {style: {b: true}},
261 - ),
262 - ).toEqual({b: null});
263 - expect(
264 - diff(
265 - {style: undefined},
266 - {style: {a: '#ffffff', b: 1}},
267 - {style: {b: true}},
268 - ),
269 - ).toEqual({b: 1});
270 - expect(
271 - diff({style: undefined}, {style: undefined}, {style: {b: true}}),
272 - ).toEqual(null);
273 - });
274 -
275 - it('should work with empty styles', () => {
276 - expect(diff({a: 1, c: 3}, {}, {a: true, b: true})).toEqual({a: null});
277 - expect(diff({}, {a: 1, c: 3}, {a: true, b: true})).toEqual({a: 1});
278 - expect(diff({}, {}, {a: true, b: true})).toEqual(null);
279 - });
280 -
281 - it('should flatten nested styles and predefined styles', () => {
282 - const validStyleAttribute = {someStyle: {foo: true, bar: true}};
283 -
284 - expect(
285 - diff({}, {someStyle: [{foo: 1}, {bar: 2}]}, validStyleAttribute),
286 - ).toEqual({foo: 1, bar: 2});
287 -
288 - expect(
289 - diff({someStyle: [{foo: 1}, {bar: 2}]}, {}, validStyleAttribute),
290 - ).toEqual({foo: null, bar: null});
291 -
292 - const barStyle = {
293 - bar: 3,
294 - };
295 -
296 - expect(
297 - diff(
298 - {},
299 - {someStyle: [[{foo: 1}, {foo: 2}], barStyle]},
300 - validStyleAttribute,
301 - ),
302 - ).toEqual({foo: 2, bar: 3});
303 - });
304 -
305 - it('should reset a value to a previous if it is removed', () => {
306 - const validStyleAttribute = {someStyle: {foo: true, bar: true}};
307 -
308 - expect(
309 - diff(
310 - {someStyle: [{foo: 1}, {foo: 3}]},
311 - {someStyle: [{foo: 1}, {bar: 2}]},
312 - validStyleAttribute,
313 - ),
314 - ).toEqual({foo: 1, bar: 2});
315 - });
316 -
317 - it('should not clear removed props if they are still in another slot', () => {
318 - const validStyleAttribute = {someStyle: {foo: true, bar: true}};
319 -
320 - expect(
321 - diff(
322 - {someStyle: [{}, {foo: 3, bar: 2}]},
323 - {someStyle: [{foo: 3}, {bar: 2}]},
324 - validStyleAttribute,
325 - ),
326 - ).toEqual({foo: 3}); // this should ideally be null. heuristic tradeoff.
327 -
328 - expect(
329 - diff(
330 - {someStyle: [{}, {foo: 3, bar: 2}]},
331 - {someStyle: [{foo: 1, bar: 1}, {bar: 2}]},
332 - validStyleAttribute,
333 - ),
334 - ).toEqual({bar: 2, foo: 1});
335 - });
336 -
337 - it('should clear a prop if a later style is explicit null/undefined', () => {
338 - const validStyleAttribute = {someStyle: {foo: true, bar: true}};
339 - expect(
340 - diff(
341 - {someStyle: [{}, {foo: 3, bar: 2}]},
342 - {someStyle: [{foo: 1}, {bar: 2, foo: null}]},
343 - validStyleAttribute,
344 - ),
345 - ).toEqual({foo: null});
346 -
347 - expect(
348 - diff(
349 - {someStyle: [{foo: 3}, {foo: null, bar: 2}]},
350 - {someStyle: [{foo: null}, {bar: 2}]},
351 - validStyleAttribute,
352 - ),
353 - ).toEqual({foo: null});
354 -
355 - expect(
356 - diff(
357 - {someStyle: [{foo: 1}, {foo: null}]},
358 - {someStyle: [{foo: 2}, {foo: null}]},
359 - validStyleAttribute,
360 - ),
361 - ).toEqual({foo: null}); // this should ideally be null. heuristic.
362 -
363 - // Test the same case with object equality because an early bailout doesn't
364 - // work in this case.
365 - const fooObj = {foo: 3};
366 - expect(
367 - diff(
368 - {someStyle: [{foo: 1}, fooObj]},
369 - {someStyle: [{foo: 2}, fooObj]},
370 - validStyleAttribute,
371 - ),
372 - ).toEqual({foo: 3}); // this should ideally be null. heuristic.
373 -
374 - expect(
375 - diff(
376 - {someStyle: [{foo: 1}, {foo: 3}]},
377 - {someStyle: [{foo: 2}, {foo: undefined}]},
378 - validStyleAttribute,
379 - ),
380 - ).toEqual({foo: null}); // this should ideally be null. heuristic.
381 - });
382 -
383 - it('handles attributes defined multiple times', () => {
384 - const validAttributes = {foo: true, style: {foo: true}};
385 - expect(diff({}, {foo: 4, style: {foo: 2}}, validAttributes)).toEqual({
386 - foo: 2,
387 - });
388 - expect(diff({foo: 4}, {style: {foo: 2}}, validAttributes)).toEqual({
389 - foo: 2,
390 - });
391 - expect(diff({style: {foo: 2}}, {foo: 4}, validAttributes)).toEqual({
392 - foo: 4,
393 - });
394 - });
395 -
396 - // Function properties are just markers to native that events should be sent.
397 - it('should convert functions to booleans', () => {
398 - // Note that if the property changes from one function to another, we don't
399 - // need to send an update.
400 - expect(
401 - diff(
402 - {
403 - a: function () {
404 - return 1;
405 - },
406 - b: function () {
407 - return 2;
408 - },
409 - c: 3,
410 - },
411 - {
412 - b: function () {
413 - return 9;
414 - },
415 - c: function () {
416 - return 3;
417 - },
418 - },
419 - {a: true, b: true, c: true},
420 - ),
421 - ).toEqual({a: null, c: true});
422 - });
423 -
424 - it('should skip changed functions', () => {
425 - expect(
426 - diff(
427 - {
428 - a: function () {
429 - return 1;
430 - },
431 - },
432 - {
433 - a: function () {
434 - return 9;
435 - },
436 - },
437 - {a: true},
438 - ),
439 - ).toEqual(null);
440 - });
441 -
442 - it('should skip deeply-nested changed functions', () => {
443 - expect(
444 - diff(
445 - {
446 - wrapper: {
447 - a: function () {
448 - return 1;
449 - },
450 - },
451 - },
452 - {
453 - wrapper: {
454 - a: function () {
455 - return 9;
456 - },
457 - },
458 - },
459 - {wrapper: true},
460 - ),
461 - ).toEqual(null);
462 - });
463 -
464 - it('should use the process function config when prop is a function', () => {
465 - const process = jest.fn(a => a);
466 - const nextFunction = function () {};
467 - expect(
468 - diff(
469 - {
470 - a: function () {},
471 - },
472 - {
473 - a: nextFunction,
474 - },
475 - {a: {process}},
476 - ),
477 - ).toEqual({a: nextFunction});
478 - expect(process).toBeCalled();
479 - });
480 -});
scripts/flow/react-native-host-hooks.js
+10
@@ -32,6 +32,7 @@ type __MeasureLayoutOnSuccessCallback = (
32 type __ReactNativeBaseComponentViewConfig = any;
33 type __ViewConfigGetter = any;
34 type __ViewConfig = any;
35 +type __AttributeConfiguration = any;
36
37 // libdefs cannot actually import. This is supposed to be the type imported
38 // from 'react-native-renderer/src/legacy-events/TopLevelEventTypes';
@@ -203,6 +204,15 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
204 declare export function getInternalInstanceHandleFromPublicInstance(
205 publicInstance: PublicInstance,
206 ): ?Object;
207 + declare export function createAttributePayload(
208 + props: Object,
209 + validAttributes: __AttributeConfiguration,
210 + ): null | Object;
211 + declare export function diffAttributePayloads(
212 + prevProps: Object,
213 + nextProps: Object,
214 + validAttributes: __AttributeConfiguration,
215 + ): null | Object;
216 }
217
218 declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore' {