mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
[react-native] Consume ReactNativeAttributePayloadFabric from ReactNativePrivateInterface (#33616)
## Summary
ReactNativeAttributePayloadFabric was synced to react-native in
0e42d33cbc.
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`
This commit is contained in:
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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<NestedNode> | 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<NestedNode>,
|
||||
nextArray: Array<NestedNode>,
|
||||
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,
|
||||
);
|
||||
}
|
||||
@@ -34,15 +34,6 @@ export type AttributeType<T, V> =
|
||||
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<{
|
||||
|
||||
@@ -59,4 +59,10 @@ module.exports = {
|
||||
get createPublicRootInstance() {
|
||||
return require('./createPublicRootInstance').default;
|
||||
},
|
||||
get createAttributePayload() {
|
||||
return require('./createAttributePayload').default;
|
||||
},
|
||||
get diffAttributePayloads() {
|
||||
return require('./diffAttributePayloads').default;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -184,6 +184,10 @@ describe('ReactFabric', () => {
|
||||
nativeFabricUIManager.cloneNodeWithNewChildrenAndProps,
|
||||
).not.toBeCalled();
|
||||
|
||||
jest
|
||||
.spyOn(ReactNativePrivateInterface, 'diffAttributePayloads')
|
||||
.mockReturnValue({bar: 'b'});
|
||||
|
||||
await act(() => {
|
||||
ReactFabric.render(
|
||||
<Text foo="a" bar="b">
|
||||
@@ -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(
|
||||
<Text foo="b" bar="b">
|
||||
@@ -612,7 +619,7 @@ describe('ReactFabric', () => {
|
||||
ReactFabric.render(<Component chars={before} />, 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(<Component chars={after} />, 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"}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
10
scripts/flow/react-native-host-hooks.js
vendored
10
scripts/flow/react-native-host-hooks.js
vendored
@@ -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' {
|
||||
|
||||
Reference in New Issue
Block a user