From 3d41f9b6fb56851edf0ac98e74347c3bd094026f Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Mon, 23 Jun 2025 12:59:02 +0100 Subject: [PATCH] [react-native] Consume ReactNativeAttributePayloadFabric from ReactNativePrivateInterface --- .../src/ReactFiberConfigFabric.js | 16 +- .../src/ReactNativeAttributePayloadFabric.js | 514 ------------------ .../src/ReactNativeTypes.js | 11 +- .../ReactNativePrivateInterface.js | 6 + .../ReactPrivate/createAttributePayload.js | 18 + .../ReactPrivate/diffAttributePayloads.js | 22 + .../__tests__/ReactFabric-test.internal.js | 19 +- ...iveAttributePayloadFabric-test.internal.js | 480 ---------------- scripts/flow/react-native-host-hooks.js | 10 + 9 files changed, 82 insertions(+), 1014 deletions(-) delete mode 100644 packages/react-native-renderer/src/ReactNativeAttributePayloadFabric.js create mode 100644 packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/createAttributePayload.js create mode 100644 packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/diffAttributePayloads.js delete mode 100644 packages/react-native-renderer/src/__tests__/ReactNativeAttributePayloadFabric-test.internal.js diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index 9a661ee7414b3..f6655c0cc91af 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -12,7 +12,6 @@ import type { TouchedViewDataAtPoint, ViewConfig, } from './ReactNativeTypes'; -import {create, diff} from './ReactNativeAttributePayloadFabric'; import {dispatchEvent} from './ReactFabricEventEmitter'; import { NoEventPriority, @@ -35,6 +34,8 @@ import { deepFreezeAndThrowOnMutationInDev, createPublicInstance, createPublicTextInstance, + createAttributePayload, + diffAttributePayloads, type PublicInstance as ReactNativePublicInstance, type PublicTextInstance, type PublicRootInstance, @@ -190,7 +191,10 @@ export function createInstance( } } - const updatePayload = create(props, viewConfig.validAttributes); + const updatePayload = createAttributePayload( + props, + viewConfig.validAttributes, + ); const node = createNode( tag, // reactTag @@ -456,7 +460,11 @@ export function cloneInstance( newChildSet: ?ChildSet, ): Instance { const viewConfig = instance.canonical.viewConfig; - const updatePayload = diff(oldProps, newProps, viewConfig.validAttributes); + const updatePayload = diffAttributePayloads( + 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. @@ -505,7 +513,7 @@ export function cloneHiddenInstance( ): Instance { const viewConfig = instance.canonical.viewConfig; const node = instance.node; - const updatePayload = create( + const updatePayload = createAttributePayload( {style: {display: 'none'}}, viewConfig.validAttributes, ); diff --git a/packages/react-native-renderer/src/ReactNativeAttributePayloadFabric.js b/packages/react-native-renderer/src/ReactNativeAttributePayloadFabric.js deleted file mode 100644 index 88ff3c73099f3..0000000000000 --- a/packages/react-native-renderer/src/ReactNativeAttributePayloadFabric.js +++ /dev/null @@ -1,514 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -// Modules provided by RN: -import { - deepDiffer, - flattenStyle, -} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; -import isArray from 'shared/isArray'; - -import type {AttributeConfiguration} from './ReactNativeTypes'; - -const emptyObject = {}; - -/** - * Create a payload that contains all the updates between two sets of props. - * - * These helpers are all encapsulated into a single module, because they use - * mutation as a performance optimization which leads to subtle shared - * dependencies between the code paths. To avoid this mutable state leaking - * across modules, I've kept them isolated to this module. - */ - -type NestedNode = Array | Object; - -// Tracks removed keys -let removedKeys: {[string]: boolean} | null = null; -let removedKeyCount = 0; - -const deepDifferOptions = { - unsafelyIgnoreFunctions: true, -}; - -function defaultDiffer(prevProp: mixed, nextProp: mixed): boolean { - if (typeof nextProp !== 'object' || nextProp === null) { - // Scalars have already been checked for equality - return true; - } else { - // For objects and arrays, the default diffing algorithm is a deep compare - return deepDiffer(prevProp, nextProp, deepDifferOptions); - } -} - -function restoreDeletedValuesInNestedArray( - updatePayload: Object, - node: NestedNode, - validAttributes: AttributeConfiguration, -) { - if (isArray(node)) { - let i = node.length; - while (i-- && removedKeyCount > 0) { - restoreDeletedValuesInNestedArray( - updatePayload, - node[i], - validAttributes, - ); - } - } else if (node && removedKeyCount > 0) { - const obj = node; - for (const propKey in removedKeys) { - // $FlowFixMe[incompatible-use] found when upgrading Flow - if (!removedKeys[propKey]) { - continue; - } - let nextProp = obj[propKey]; - if (nextProp === undefined) { - continue; - } - - const attributeConfig = validAttributes[propKey]; - if (!attributeConfig) { - continue; // not a valid native prop - } - - if (typeof nextProp === 'function') { - // $FlowFixMe[incompatible-type] found when upgrading Flow - nextProp = true; - } - if (typeof nextProp === 'undefined') { - // $FlowFixMe[incompatible-type] found when upgrading Flow - nextProp = null; - } - - if (typeof attributeConfig !== 'object') { - // case: !Object is the default case - updatePayload[propKey] = nextProp; - } else if ( - typeof attributeConfig.diff === 'function' || - typeof attributeConfig.process === 'function' - ) { - // case: CustomAttributeConfiguration - const nextValue = - typeof attributeConfig.process === 'function' - ? attributeConfig.process(nextProp) - : nextProp; - updatePayload[propKey] = nextValue; - } - // $FlowFixMe[incompatible-use] found when upgrading Flow - removedKeys[propKey] = false; - removedKeyCount--; - } - } -} - -function diffNestedArrayProperty( - updatePayload: null | Object, - prevArray: Array, - nextArray: Array, - validAttributes: AttributeConfiguration, -): null | Object { - const minLength = - prevArray.length < nextArray.length ? prevArray.length : nextArray.length; - let i; - for (i = 0; i < minLength; i++) { - // Diff any items in the array in the forward direction. Repeated keys - // will be overwritten by later values. - updatePayload = diffNestedProperty( - updatePayload, - prevArray[i], - nextArray[i], - validAttributes, - ); - } - for (; i < prevArray.length; i++) { - // Clear out all remaining properties. - updatePayload = clearNestedProperty( - updatePayload, - prevArray[i], - validAttributes, - ); - } - for (; i < nextArray.length; i++) { - // Add all remaining properties - const nextProp = nextArray[i]; - if (!nextProp) { - continue; - } - updatePayload = addNestedProperty(updatePayload, nextProp, validAttributes); - } - return updatePayload; -} - -function diffNestedProperty( - updatePayload: null | Object, - prevProp: NestedNode, - nextProp: NestedNode, - validAttributes: AttributeConfiguration, -): null | Object { - if (!updatePayload && prevProp === nextProp) { - // If no properties have been added, then we can bail out quickly on object - // equality. - return updatePayload; - } - - if (!prevProp || !nextProp) { - if (nextProp) { - return addNestedProperty(updatePayload, nextProp, validAttributes); - } - if (prevProp) { - return clearNestedProperty(updatePayload, prevProp, validAttributes); - } - return updatePayload; - } - - if (!isArray(prevProp) && !isArray(nextProp)) { - // Both are leaves, we can diff the leaves. - return diffProperties(updatePayload, prevProp, nextProp, validAttributes); - } - - if (isArray(prevProp) && isArray(nextProp)) { - // Both are arrays, we can diff the arrays. - return diffNestedArrayProperty( - updatePayload, - prevProp, - nextProp, - validAttributes, - ); - } - - if (isArray(prevProp)) { - return diffProperties( - updatePayload, - flattenStyle(prevProp), - nextProp, - validAttributes, - ); - } - - return diffProperties( - updatePayload, - prevProp, - flattenStyle(nextProp), - validAttributes, - ); -} - -/** - * clearNestedProperty takes a single set of props and valid attributes. It - * adds a null sentinel to the updatePayload, for each prop key. - */ -function clearNestedProperty( - updatePayload: null | Object, - prevProp: NestedNode, - validAttributes: AttributeConfiguration, -): null | Object { - if (!prevProp) { - return updatePayload; - } - - if (!isArray(prevProp)) { - // Add each property of the leaf. - return clearProperties(updatePayload, prevProp, validAttributes); - } - - for (let i = 0; i < prevProp.length; i++) { - // Add all the properties of the array. - updatePayload = clearNestedProperty( - updatePayload, - prevProp[i], - validAttributes, - ); - } - return updatePayload; -} - -/** - * diffProperties takes two sets of props and a set of valid attributes - * and write to updatePayload the values that changed or were deleted. - * If no updatePayload is provided, a new one is created and returned if - * anything changed. - */ -function diffProperties( - updatePayload: null | Object, - prevProps: Object, - nextProps: Object, - validAttributes: AttributeConfiguration, -): null | Object { - let attributeConfig; - let nextProp; - let prevProp; - - for (const propKey in nextProps) { - attributeConfig = validAttributes[propKey]; - if (!attributeConfig) { - continue; // not a valid native prop - } - - prevProp = prevProps[propKey]; - nextProp = nextProps[propKey]; - - if (typeof nextProp === 'function') { - const attributeConfigHasProcess = - typeof attributeConfig === 'object' && - typeof attributeConfig.process === 'function'; - if (!attributeConfigHasProcess) { - // functions are converted to booleans as markers that the associated - // events should be sent from native. - nextProp = (true: any); - // If nextProp is not a function, then don't bother changing prevProp - // since nextProp will win and go into the updatePayload regardless. - if (typeof prevProp === 'function') { - prevProp = (true: any); - } - } - } - - // An explicit value of undefined is treated as a null because it overrides - // any other preceding value. - if (typeof nextProp === 'undefined') { - nextProp = (null: any); - if (typeof prevProp === 'undefined') { - prevProp = (null: any); - } - } - - if (removedKeys) { - removedKeys[propKey] = false; - } - - if (updatePayload && updatePayload[propKey] !== undefined) { - // Something else already triggered an update to this key because another - // value diffed. Since we're now later in the nested arrays our value is - // more important so we need to calculate it and override the existing - // value. It doesn't matter if nothing changed, we'll set it anyway. - - // Pattern match on: attributeConfig - if (typeof attributeConfig !== 'object') { - // case: !Object is the default case - updatePayload[propKey] = nextProp; - } else if ( - typeof attributeConfig.diff === 'function' || - typeof attributeConfig.process === 'function' - ) { - // case: CustomAttributeConfiguration - const nextValue = - typeof attributeConfig.process === 'function' - ? attributeConfig.process(nextProp) - : nextProp; - updatePayload[propKey] = nextValue; - } - continue; - } - - if (prevProp === nextProp) { - continue; // nothing changed - } - - // Pattern match on: attributeConfig - if (typeof attributeConfig !== 'object') { - // case: !Object is the default case - if (defaultDiffer(prevProp, nextProp)) { - // a normal leaf has changed - (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[ - propKey - ] = nextProp; - } - } else if ( - typeof attributeConfig.diff === 'function' || - typeof attributeConfig.process === 'function' - ) { - // case: CustomAttributeConfiguration - const shouldUpdate = - prevProp === undefined || - (typeof attributeConfig.diff === 'function' - ? attributeConfig.diff(prevProp, nextProp) - : defaultDiffer(prevProp, nextProp)); - if (shouldUpdate) { - const nextValue = - typeof attributeConfig.process === 'function' - ? // $FlowFixMe[incompatible-use] found when upgrading Flow - attributeConfig.process(nextProp) - : nextProp; - (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[ - propKey - ] = nextValue; - } - } else { - // default: fallthrough case when nested properties are defined - removedKeys = null; - removedKeyCount = 0; - // We think that attributeConfig is not CustomAttributeConfiguration at - // this point so we assume it must be AttributeConfiguration. - updatePayload = diffNestedProperty( - updatePayload, - prevProp, - nextProp, - ((attributeConfig: any): AttributeConfiguration), - ); - if (removedKeyCount > 0 && updatePayload) { - restoreDeletedValuesInNestedArray( - updatePayload, - nextProp, - ((attributeConfig: any): AttributeConfiguration), - ); - removedKeys = null; - } - } - } - - // Also iterate through all the previous props to catch any that have been - // removed and make sure native gets the signal so it can reset them to the - // default. - for (const propKey in prevProps) { - if (nextProps[propKey] !== undefined) { - continue; // we've already covered this key in the previous pass - } - attributeConfig = validAttributes[propKey]; - if (!attributeConfig) { - continue; // not a valid native prop - } - - if (updatePayload && updatePayload[propKey] !== undefined) { - // This was already updated to a diff result earlier. - continue; - } - - prevProp = prevProps[propKey]; - if (prevProp === undefined) { - continue; // was already empty anyway - } - // Pattern match on: attributeConfig - if ( - typeof attributeConfig !== 'object' || - typeof attributeConfig.diff === 'function' || - typeof attributeConfig.process === 'function' - ) { - // case: CustomAttributeConfiguration | !Object - // Flag the leaf property for removal by sending a sentinel. - (updatePayload || (updatePayload = ({}: {[string]: $FlowFixMe})))[ - propKey - ] = null; - if (!removedKeys) { - removedKeys = ({}: {[string]: boolean}); - } - if (!removedKeys[propKey]) { - removedKeys[propKey] = true; - removedKeyCount++; - } - } else { - // default: - // This is a nested attribute configuration where all the properties - // were removed so we need to go through and clear out all of them. - updatePayload = clearNestedProperty( - updatePayload, - prevProp, - ((attributeConfig: any): AttributeConfiguration), - ); - } - } - return updatePayload; -} - -function addNestedProperty( - payload: null | Object, - props: Object, - validAttributes: AttributeConfiguration, -): null | Object { - // Flatten nested style props. - if (isArray(props)) { - for (let i = 0; i < props.length; i++) { - payload = addNestedProperty(payload, props[i], validAttributes); - } - return payload; - } - - for (const propKey in props) { - const prop = props[propKey]; - - const attributeConfig = ((validAttributes[ - propKey - ]: any): AttributeConfiguration); - - if (attributeConfig == null) { - continue; - } - - let newValue; - - if (prop === undefined) { - // Discard the prop if it was previously defined. - if (payload && payload[propKey] !== undefined) { - newValue = null; - } else { - continue; - } - } else if (typeof attributeConfig === 'object') { - if (typeof attributeConfig.process === 'function') { - // An atomic prop with custom processing. - newValue = attributeConfig.process(prop); - } else if (typeof attributeConfig.diff === 'function') { - // An atomic prop with custom diffing. We don't need to do diffing when adding props. - newValue = prop; - } - } else { - if (typeof prop === 'function') { - // A function prop. It represents an event handler. Pass it to native as 'true'. - newValue = true; - } else { - // An atomic prop. Doesn't need to be flattened. - newValue = prop; - } - } - - if (newValue !== undefined) { - if (!payload) { - payload = ({}: {[string]: $FlowFixMe}); - } - payload[propKey] = newValue; - continue; - } - - payload = addNestedProperty(payload, prop, attributeConfig); - } - - return payload; -} - -/** - * clearProperties clears all the previous props by adding a null sentinel - * to the payload for each valid key. - */ -function clearProperties( - updatePayload: null | Object, - prevProps: Object, - validAttributes: AttributeConfiguration, -): null | Object { - return diffProperties(updatePayload, prevProps, emptyObject, validAttributes); -} - -export function create( - props: Object, - validAttributes: AttributeConfiguration, -): null | Object { - return addNestedProperty(null, props, validAttributes); -} - -export function diff( - prevProps: Object, - nextProps: Object, - validAttributes: AttributeConfiguration, -): null | Object { - return diffProperties( - null, // updatePayload - prevProps, - nextProps, - validAttributes, - ); -} diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index eb5483d1b85a5..b19ce626b55f8 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -34,15 +34,6 @@ export type AttributeType = export type AnyAttributeType = AttributeType<$FlowFixMe, $FlowFixMe>; export type AttributeConfiguration = $ReadOnly<{ - [propName: string]: AnyAttributeType, - style: $ReadOnly<{ - [propName: string]: AnyAttributeType, - ... - }>, - ... -}>; - -export type PartialAttributeConfiguration = $ReadOnly<{ [propName: string]: AnyAttributeType, style?: $ReadOnly<{ [propName: string]: AnyAttributeType, @@ -83,7 +74,7 @@ export type PartialViewConfig = $ReadOnly<{ directEventTypes?: ViewConfig['directEventTypes'], supportsRawText?: boolean, uiViewClassName: string, - validAttributes?: PartialAttributeConfiguration, + validAttributes?: AttributeConfiguration, }>; type InspectorDataProps = $ReadOnly<{ diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js index 5800e445dd30b..0d76206124eeb 100644 --- a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/ReactNativePrivateInterface.js @@ -59,4 +59,10 @@ module.exports = { get createPublicRootInstance() { return require('./createPublicRootInstance').default; }, + get createAttributePayload() { + return require('./createAttributePayload').default; + }, + get diffAttributePayloads() { + return require('./diffAttributePayloads').default; + }, }; diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/createAttributePayload.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/createAttributePayload.js new file mode 100644 index 0000000000000..78ce43e1d9a67 --- /dev/null +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/createAttributePayload.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + */ + +import type {AttributeConfiguration} from '../../../../ReactNativeTypes'; + +export default function create( + props: Object, + validAttributes: AttributeConfiguration, +): null | Object { + const {children, ...propsToPass} = props; + return propsToPass; +} diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/diffAttributePayloads.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/diffAttributePayloads.js new file mode 100644 index 0000000000000..9a1756ef9f948 --- /dev/null +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/diffAttributePayloads.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + */ + +import type {AttributeConfiguration} from '../../../../ReactNativeTypes'; + +import deepDiffer from './deepDiffer'; + +export default function diff( + prevProps: Object, + nextProps: Object, + validAttributes: AttributeConfiguration, +): null | Object { + const {children: _prevChildren, ...prevPropsPassed} = prevProps; + const {children: _nextChildren, ...nextPropsToPass} = nextProps; + return deepDiffer(prevPropsPassed, nextPropsToPass) ? nextPropsToPass : null; +} diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index 5fbf5a9f2cdf8..f8dbcfd17e8c7 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -184,6 +184,10 @@ describe('ReactFabric', () => { nativeFabricUIManager.cloneNodeWithNewChildrenAndProps, ).not.toBeCalled(); + jest + .spyOn(ReactNativePrivateInterface, 'diffAttributePayloads') + .mockReturnValue({bar: 'b'}); + await act(() => { ReactFabric.render( @@ -203,6 +207,9 @@ describe('ReactFabric', () => { RCTText {"foo":"a","bar":"b"} RCTRawText {"text":"1"}`); + jest + .spyOn(ReactNativePrivateInterface, 'diffAttributePayloads') + .mockReturnValue({foo: 'b'}); await act(() => { ReactFabric.render( @@ -612,7 +619,7 @@ describe('ReactFabric', () => { ReactFabric.render(, 11, null, true); }); expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11 - RCTView null + RCTView {} RCTView {"title":"a"} RCTView {"title":"b"} RCTView {"title":"c"} @@ -638,7 +645,7 @@ describe('ReactFabric', () => { ReactFabric.render(, 11, null, true); }); expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11 - RCTView null + RCTView {} RCTView {"title":"m"} RCTView {"title":"x"} RCTView {"title":"h"} @@ -700,8 +707,8 @@ describe('ReactFabric', () => { }); expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe( `11 - RCTView null - RCTView null + RCTView {} + RCTView {} RCTView {"title":"a"} RCTView {"title":"b"} RCTView {"title":"c"} @@ -732,8 +739,8 @@ describe('ReactFabric', () => { }); }); expect(nativeFabricUIManager.__dumpHierarchyForJestTestsOnly()).toBe(`11 - RCTView null - RCTView null + RCTView {} + RCTView {} RCTView {"title":"m"} RCTView {"title":"x"} RCTView {"title":"h"} diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeAttributePayloadFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeAttributePayloadFabric-test.internal.js deleted file mode 100644 index 33f34f6d25515..0000000000000 --- a/packages/react-native-renderer/src/__tests__/ReactNativeAttributePayloadFabric-test.internal.js +++ /dev/null @@ -1,480 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @jest-environment node - */ -'use strict'; - -const {diff, create} = require('../ReactNativeAttributePayloadFabric'); - -describe('ReactNativeAttributePayloadFabric.create', () => { - it('should work with simple example', () => { - expect(create({b: 2, c: 3}, {a: true, b: true})).toEqual({ - b: 2, - }); - }); - - it('should work with complex example', () => { - const validAttributes = { - style: { - position: true, - zIndex: true, - flexGrow: true, - flexShrink: true, - flexDirection: true, - overflow: true, - backgroundColor: true, - }, - }; - - expect( - create( - { - style: [ - { - flexGrow: 1, - flexShrink: 1, - flexDirection: 'row', - overflow: 'scroll', - }, - [ - {position: 'relative', zIndex: 2}, - {flexGrow: 0}, - {backgroundColor: 'red'}, - ], - ], - }, - validAttributes, - ), - ).toEqual({ - flexGrow: 0, - flexShrink: 1, - flexDirection: 'row', - overflow: 'scroll', - position: 'relative', - zIndex: 2, - backgroundColor: 'red', - }); - }); - - it('should nullify previously defined style prop that is subsequently set to null or undefined', () => { - expect( - create({style: [{a: 0}, {a: undefined}]}, {style: {a: true}}), - ).toEqual({a: null}); - expect(create({style: [{a: 0}, {a: null}]}, {style: {a: true}})).toEqual({ - a: null, - }); - }); - - it('should ignore non-style fields that are set to undefined', () => { - expect(create({}, {a: true})).toEqual(null); - expect(create({a: undefined}, {a: true})).toEqual(null); - expect(create({a: undefined, b: undefined}, {a: true, b: true})).toEqual( - null, - ); - expect( - create({a: undefined, b: undefined, c: 1}, {a: true, b: true}), - ).toEqual(null); - expect( - create({a: undefined, b: undefined, c: 1}, {a: true, b: true, c: true}), - ).toEqual({c: 1}); - expect( - create({a: 1, b: undefined, c: 2}, {a: true, b: true, c: true}), - ).toEqual({a: 1, c: 2}); - }); - - it('should ignore invalid fields', () => { - expect(create({b: 2}, {})).toEqual(null); - }); - - it('should not use the diff attribute', () => { - const diffA = jest.fn(); - expect(create({a: [2]}, {a: {diff: diffA}})).toEqual({a: [2]}); - expect(diffA).not.toBeCalled(); - }); - - it('should use the process attribute', () => { - const processA = jest.fn(a => a + 1); - expect(create({a: 2}, {a: {process: processA}})).toEqual({a: 3}); - expect(processA).toBeCalledWith(2); - }); - - it('should use the process attribute for functions as well', () => { - const process = x => x; - const nextFunction = () => {}; - expect(create({a: nextFunction}, {a: {process}})).toEqual({ - a: nextFunction, - }); - }); - - it('should work with undefined styles', () => { - expect(create({style: undefined}, {style: {b: true}})).toEqual(null); - expect(create({style: {a: '#ffffff', b: 1}}, {style: {b: true}})).toEqual({ - b: 1, - }); - }); - - it('should flatten nested styles and predefined styles', () => { - const validStyleAttribute = {someStyle: {foo: true, bar: true}}; - expect( - create({someStyle: [{foo: 1}, {bar: 2}]}, validStyleAttribute), - ).toEqual({foo: 1, bar: 2}); - expect(create({}, validStyleAttribute)).toEqual(null); - const barStyle = { - bar: 3, - }; - expect( - create( - {someStyle: [[{foo: 1}, {foo: 2}], barStyle]}, - validStyleAttribute, - ), - ).toEqual({foo: 2, bar: 3}); - }); - - it('should not flatten nested props if attribute config is a primitive or only has diff/process', () => { - expect(create({a: {foo: 1, bar: 2}}, {a: true})).toEqual({ - a: {foo: 1, bar: 2}, - }); - expect(create({a: [{foo: 1}, {bar: 2}]}, {a: true})).toEqual({ - a: [{foo: 1}, {bar: 2}], - }); - expect(create({a: {foo: 1, bar: 2}}, {a: {diff: a => a}})).toEqual({ - a: {foo: 1, bar: 2}, - }); - expect( - create({a: [{foo: 1}, {bar: 2}]}, {a: {diff: a => a, process: a => a}}), - ).toEqual({a: [{foo: 1}, {bar: 2}]}); - }); - - it('handles attributes defined multiple times', () => { - const validAttributes = {foo: true, style: {foo: true}}; - expect(create({foo: 4, style: {foo: 2}}, validAttributes)).toEqual({ - foo: 2, - }); - expect(create({style: {foo: 2}}, validAttributes)).toEqual({ - foo: 2, - }); - expect(create({style: {foo: 2}, foo: 4}, validAttributes)).toEqual({ - foo: 4, - }); - expect(create({foo: 4, style: {foo: null}}, validAttributes)).toEqual({ - foo: null, // this should ideally be null. - }); - expect( - create({foo: 4, style: [{foo: null}, {foo: 5}]}, validAttributes), - ).toEqual({ - foo: 5, - }); - }); - - // Function properties are just markers to native that events should be sent. - it('should convert functions to booleans', () => { - expect( - create( - { - a: function () { - return 9; - }, - b: function () { - return 3; - }, - }, - {a: true, b: true}, - ), - ).toEqual({a: true, b: true}); - }); -}); - -describe('ReactNativeAttributePayloadFabric.diff', () => { - it('should work with simple example', () => { - expect(diff({a: 1, c: 3}, {b: 2, c: 3}, {a: true, b: true})).toEqual({ - a: null, - b: 2, - }); - }); - - it('should skip fields that are equal', () => { - expect( - diff( - {a: 1, b: 'two', c: true, d: false, e: undefined, f: 0}, - {a: 1, b: 'two', c: true, d: false, e: undefined, f: 0}, - {a: true, b: true, c: true, d: true, e: true, f: true}, - ), - ).toEqual(null); - }); - - it('should remove fields', () => { - expect(diff({a: 1}, {}, {a: true})).toEqual({a: null}); - }); - - it('should remove fields that are set to undefined', () => { - expect(diff({a: 1}, {a: undefined}, {a: true})).toEqual({a: null}); - }); - - it('should ignore invalid fields', () => { - expect(diff({a: 1}, {b: 2}, {})).toEqual(null); - }); - - it('should use the diff attribute', () => { - const diffA = jest.fn((a, b) => true); - const diffB = jest.fn((a, b) => false); - expect( - diff( - {a: [1], b: [3]}, - {a: [2], b: [4]}, - {a: {diff: diffA}, b: {diff: diffB}}, - ), - ).toEqual({a: [2]}); - expect(diffA).toBeCalledWith([1], [2]); - expect(diffB).toBeCalledWith([3], [4]); - }); - - it('should not use the diff attribute on addition/removal', () => { - const diffA = jest.fn(); - const diffB = jest.fn(); - expect( - diff({a: [1]}, {b: [2]}, {a: {diff: diffA}, b: {diff: diffB}}), - ).toEqual({a: null, b: [2]}); - expect(diffA).not.toBeCalled(); - expect(diffB).not.toBeCalled(); - }); - - it('should do deep diffs of Objects by default', () => { - expect( - diff( - {a: [1], b: {k: [3, 4]}, c: {k: [4, 4]}}, - {a: [2], b: {k: [3, 4]}, c: {k: [4, 5]}}, - {a: true, b: true, c: true}, - ), - ).toEqual({a: [2], c: {k: [4, 5]}}); - }); - - it('should work with undefined styles', () => { - expect( - diff( - {style: {a: '#ffffff', b: 1}}, - {style: undefined}, - {style: {b: true}}, - ), - ).toEqual({b: null}); - expect( - diff( - {style: undefined}, - {style: {a: '#ffffff', b: 1}}, - {style: {b: true}}, - ), - ).toEqual({b: 1}); - expect( - diff({style: undefined}, {style: undefined}, {style: {b: true}}), - ).toEqual(null); - }); - - it('should work with empty styles', () => { - expect(diff({a: 1, c: 3}, {}, {a: true, b: true})).toEqual({a: null}); - expect(diff({}, {a: 1, c: 3}, {a: true, b: true})).toEqual({a: 1}); - expect(diff({}, {}, {a: true, b: true})).toEqual(null); - }); - - it('should flatten nested styles and predefined styles', () => { - const validStyleAttribute = {someStyle: {foo: true, bar: true}}; - - expect( - diff({}, {someStyle: [{foo: 1}, {bar: 2}]}, validStyleAttribute), - ).toEqual({foo: 1, bar: 2}); - - expect( - diff({someStyle: [{foo: 1}, {bar: 2}]}, {}, validStyleAttribute), - ).toEqual({foo: null, bar: null}); - - const barStyle = { - bar: 3, - }; - - expect( - diff( - {}, - {someStyle: [[{foo: 1}, {foo: 2}], barStyle]}, - validStyleAttribute, - ), - ).toEqual({foo: 2, bar: 3}); - }); - - it('should reset a value to a previous if it is removed', () => { - const validStyleAttribute = {someStyle: {foo: true, bar: true}}; - - expect( - diff( - {someStyle: [{foo: 1}, {foo: 3}]}, - {someStyle: [{foo: 1}, {bar: 2}]}, - validStyleAttribute, - ), - ).toEqual({foo: 1, bar: 2}); - }); - - it('should not clear removed props if they are still in another slot', () => { - const validStyleAttribute = {someStyle: {foo: true, bar: true}}; - - expect( - diff( - {someStyle: [{}, {foo: 3, bar: 2}]}, - {someStyle: [{foo: 3}, {bar: 2}]}, - validStyleAttribute, - ), - ).toEqual({foo: 3}); // this should ideally be null. heuristic tradeoff. - - expect( - diff( - {someStyle: [{}, {foo: 3, bar: 2}]}, - {someStyle: [{foo: 1, bar: 1}, {bar: 2}]}, - validStyleAttribute, - ), - ).toEqual({bar: 2, foo: 1}); - }); - - it('should clear a prop if a later style is explicit null/undefined', () => { - const validStyleAttribute = {someStyle: {foo: true, bar: true}}; - expect( - diff( - {someStyle: [{}, {foo: 3, bar: 2}]}, - {someStyle: [{foo: 1}, {bar: 2, foo: null}]}, - validStyleAttribute, - ), - ).toEqual({foo: null}); - - expect( - diff( - {someStyle: [{foo: 3}, {foo: null, bar: 2}]}, - {someStyle: [{foo: null}, {bar: 2}]}, - validStyleAttribute, - ), - ).toEqual({foo: null}); - - expect( - diff( - {someStyle: [{foo: 1}, {foo: null}]}, - {someStyle: [{foo: 2}, {foo: null}]}, - validStyleAttribute, - ), - ).toEqual({foo: null}); // this should ideally be null. heuristic. - - // Test the same case with object equality because an early bailout doesn't - // work in this case. - const fooObj = {foo: 3}; - expect( - diff( - {someStyle: [{foo: 1}, fooObj]}, - {someStyle: [{foo: 2}, fooObj]}, - validStyleAttribute, - ), - ).toEqual({foo: 3}); // this should ideally be null. heuristic. - - expect( - diff( - {someStyle: [{foo: 1}, {foo: 3}]}, - {someStyle: [{foo: 2}, {foo: undefined}]}, - validStyleAttribute, - ), - ).toEqual({foo: null}); // this should ideally be null. heuristic. - }); - - it('handles attributes defined multiple times', () => { - const validAttributes = {foo: true, style: {foo: true}}; - expect(diff({}, {foo: 4, style: {foo: 2}}, validAttributes)).toEqual({ - foo: 2, - }); - expect(diff({foo: 4}, {style: {foo: 2}}, validAttributes)).toEqual({ - foo: 2, - }); - expect(diff({style: {foo: 2}}, {foo: 4}, validAttributes)).toEqual({ - foo: 4, - }); - }); - - // Function properties are just markers to native that events should be sent. - it('should convert functions to booleans', () => { - // Note that if the property changes from one function to another, we don't - // need to send an update. - expect( - diff( - { - a: function () { - return 1; - }, - b: function () { - return 2; - }, - c: 3, - }, - { - b: function () { - return 9; - }, - c: function () { - return 3; - }, - }, - {a: true, b: true, c: true}, - ), - ).toEqual({a: null, c: true}); - }); - - it('should skip changed functions', () => { - expect( - diff( - { - a: function () { - return 1; - }, - }, - { - a: function () { - return 9; - }, - }, - {a: true}, - ), - ).toEqual(null); - }); - - it('should skip deeply-nested changed functions', () => { - expect( - diff( - { - wrapper: { - a: function () { - return 1; - }, - }, - }, - { - wrapper: { - a: function () { - return 9; - }, - }, - }, - {wrapper: true}, - ), - ).toEqual(null); - }); - - it('should use the process function config when prop is a function', () => { - const process = jest.fn(a => a); - const nextFunction = function () {}; - expect( - diff( - { - a: function () {}, - }, - { - a: nextFunction, - }, - {a: {process}}, - ), - ).toEqual({a: nextFunction}); - expect(process).toBeCalled(); - }); -}); diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index 8eec4393eed97..227c78bca24a1 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -32,6 +32,7 @@ type __MeasureLayoutOnSuccessCallback = ( type __ReactNativeBaseComponentViewConfig = any; type __ViewConfigGetter = any; type __ViewConfig = any; +type __AttributeConfiguration = any; // libdefs cannot actually import. This is supposed to be the type imported // from 'react-native-renderer/src/legacy-events/TopLevelEventTypes'; @@ -203,6 +204,15 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface' declare export function getInternalInstanceHandleFromPublicInstance( publicInstance: PublicInstance, ): ?Object; + declare export function createAttributePayload( + props: Object, + validAttributes: __AttributeConfiguration, + ): null | Object; + declare export function diffAttributePayloads( + prevProps: Object, + nextProps: Object, + validAttributes: __AttributeConfiguration, + ): null | Object; } declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInitializeCore' {