mirror of
https://github.com/zebrajr/react.git
synced 2025-12-07 00:20:28 +01:00
Stacked on #33658 and #33659. If we detect that a component is receiving only deeply equal objects, then we highlight it as potentially problematic and worth looking into. <img width="1055" alt="Screenshot 2025-06-27 at 4 15 28 PM" src="https://github.com/user-attachments/assets/e96c6a05-7fff-4fd7-b59a-36ed79f8e609" /> It's fairly conservative and can bail out for a number of reasons: - We only log it on the first parent that triggered this case since other children could be indirect causes. - If children has changed then we bail out since this component will rerender anyway. This means that it won't warn for a lot of cases that receive plain DOM children since the DOM children won't themselves get logged. - If the component's total render time including children is 100ms or less then we skip warning because rerendering might not be a big deal. - We don't warn if you have shallow equality but could memoize the JSX element itself since we don't typically recommend that and React Compiler doesn't do that. It only warns if you have nested objects too. - If the depth of the objects is deeper than like the 3 levels that we print diffs for then we wouldn't warn since we don't know if they were equal (although we might still warn on a child). - If the component had any updates scheduled on itself (e.g. setState) then we don't warn since it would rerender anyway. This should really consider Context updates too but we don't do that atm. Technically you should still memoize the incoming props even if you also had unrelated updates since it could apply to deeper bailouts.
374 lines
12 KiB
JavaScript
374 lines
12 KiB
JavaScript
/**
|
|
* 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
|
|
*/
|
|
|
|
import {OMITTED_PROP_ERROR} from 'shared/ReactFlightPropertyAccess';
|
|
|
|
import hasOwnProperty from 'shared/hasOwnProperty';
|
|
import isArray from 'shared/isArray';
|
|
import {REACT_ELEMENT_TYPE} from './ReactSymbols';
|
|
import getComponentNameFromType from './getComponentNameFromType';
|
|
|
|
const EMPTY_ARRAY = 0;
|
|
const COMPLEX_ARRAY = 1;
|
|
const PRIMITIVE_ARRAY = 2; // Primitive values only
|
|
const ENTRIES_ARRAY = 3; // Tuple arrays of string and value (like Headers, Map, etc)
|
|
function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
|
|
let kind = EMPTY_ARRAY;
|
|
for (let i = 0; i < array.length; i++) {
|
|
const value = array[i];
|
|
if (typeof value === 'object' && value !== null) {
|
|
if (
|
|
isArray(value) &&
|
|
value.length === 2 &&
|
|
typeof value[0] === 'string'
|
|
) {
|
|
// Key value tuple
|
|
if (kind !== EMPTY_ARRAY && kind !== ENTRIES_ARRAY) {
|
|
return COMPLEX_ARRAY;
|
|
}
|
|
kind = ENTRIES_ARRAY;
|
|
} else {
|
|
return COMPLEX_ARRAY;
|
|
}
|
|
} else if (typeof value === 'function') {
|
|
return COMPLEX_ARRAY;
|
|
} else if (typeof value === 'string' && value.length > 50) {
|
|
return COMPLEX_ARRAY;
|
|
} else if (kind !== EMPTY_ARRAY && kind !== PRIMITIVE_ARRAY) {
|
|
return COMPLEX_ARRAY;
|
|
} else {
|
|
kind = PRIMITIVE_ARRAY;
|
|
}
|
|
}
|
|
return kind;
|
|
}
|
|
|
|
export function addObjectToProperties(
|
|
object: Object,
|
|
properties: Array<[string, string]>,
|
|
indent: number,
|
|
prefix: string,
|
|
): void {
|
|
for (const key in object) {
|
|
if (hasOwnProperty.call(object, key) && key[0] !== '_') {
|
|
const value = object[key];
|
|
addValueToProperties(key, value, properties, indent, prefix);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function addValueToProperties(
|
|
propertyName: string,
|
|
value: mixed,
|
|
properties: Array<[string, string]>,
|
|
indent: number,
|
|
prefix: string,
|
|
): void {
|
|
let desc;
|
|
switch (typeof value) {
|
|
case 'object':
|
|
if (value === null) {
|
|
desc = 'null';
|
|
break;
|
|
} else {
|
|
if (value.$$typeof === REACT_ELEMENT_TYPE) {
|
|
// JSX
|
|
const typeName = getComponentNameFromType(value.type) || '\u2026';
|
|
const key = value.key;
|
|
const props: any = value.props;
|
|
const propsKeys = Object.keys(props);
|
|
const propsLength = propsKeys.length;
|
|
if (key == null && propsLength === 0) {
|
|
desc = '<' + typeName + ' />';
|
|
break;
|
|
}
|
|
if (
|
|
indent < 3 ||
|
|
(propsLength === 1 && propsKeys[0] === 'children' && key == null)
|
|
) {
|
|
desc = '<' + typeName + ' \u2026 />';
|
|
break;
|
|
}
|
|
properties.push([
|
|
prefix + '\xa0\xa0'.repeat(indent) + propertyName,
|
|
'<' + typeName,
|
|
]);
|
|
if (key !== null) {
|
|
addValueToProperties('key', key, properties, indent + 1, prefix);
|
|
}
|
|
let hasChildren = false;
|
|
for (const propKey in props) {
|
|
if (propKey === 'children') {
|
|
if (
|
|
props.children != null &&
|
|
(!isArray(props.children) || props.children.length > 0)
|
|
) {
|
|
hasChildren = true;
|
|
}
|
|
} else if (
|
|
hasOwnProperty.call(props, propKey) &&
|
|
propKey[0] !== '_'
|
|
) {
|
|
addValueToProperties(
|
|
propKey,
|
|
props[propKey],
|
|
properties,
|
|
indent + 1,
|
|
prefix,
|
|
);
|
|
}
|
|
}
|
|
properties.push([
|
|
'',
|
|
hasChildren ? '>\u2026</' + typeName + '>' : '/>',
|
|
]);
|
|
return;
|
|
}
|
|
// $FlowFixMe[method-unbinding]
|
|
const objectToString = Object.prototype.toString.call(value);
|
|
let objectName = objectToString.slice(8, objectToString.length - 1);
|
|
if (objectName === 'Array') {
|
|
const array: Array<any> = (value: any);
|
|
const kind = getArrayKind(array);
|
|
if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
|
|
desc = JSON.stringify(array);
|
|
break;
|
|
} else if (kind === ENTRIES_ARRAY) {
|
|
properties.push([
|
|
prefix + '\xa0\xa0'.repeat(indent) + propertyName,
|
|
'',
|
|
]);
|
|
for (let i = 0; i < array.length; i++) {
|
|
const entry = array[i];
|
|
addValueToProperties(
|
|
entry[0],
|
|
entry[1],
|
|
properties,
|
|
indent + 1,
|
|
prefix,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (objectName === 'Promise') {
|
|
if (value.status === 'fulfilled') {
|
|
// Print the inner value
|
|
const idx = properties.length;
|
|
addValueToProperties(
|
|
propertyName,
|
|
value.value,
|
|
properties,
|
|
indent,
|
|
prefix,
|
|
);
|
|
if (properties.length > idx) {
|
|
// Wrap the value or type in Promise descriptor.
|
|
const insertedEntry = properties[idx];
|
|
insertedEntry[1] =
|
|
'Promise<' + (insertedEntry[1] || 'Object') + '>';
|
|
return;
|
|
}
|
|
} else if (value.status === 'rejected') {
|
|
// Print the inner error
|
|
const idx = properties.length;
|
|
addValueToProperties(
|
|
propertyName,
|
|
value.reason,
|
|
properties,
|
|
indent,
|
|
prefix,
|
|
);
|
|
if (properties.length > idx) {
|
|
// Wrap the value or type in Promise descriptor.
|
|
const insertedEntry = properties[idx];
|
|
insertedEntry[1] = 'Rejected Promise<' + insertedEntry[1] + '>';
|
|
return;
|
|
}
|
|
}
|
|
properties.push([
|
|
'\xa0\xa0'.repeat(indent) + propertyName,
|
|
'Promise',
|
|
]);
|
|
return;
|
|
}
|
|
if (objectName === 'Object') {
|
|
const proto: any = Object.getPrototypeOf(value);
|
|
if (proto && typeof proto.constructor === 'function') {
|
|
objectName = proto.constructor.name;
|
|
}
|
|
}
|
|
properties.push([
|
|
prefix + '\xa0\xa0'.repeat(indent) + propertyName,
|
|
objectName === 'Object' ? (indent < 3 ? '' : '\u2026') : objectName,
|
|
]);
|
|
if (indent < 3) {
|
|
addObjectToProperties(value, properties, indent + 1, prefix);
|
|
}
|
|
return;
|
|
}
|
|
case 'function':
|
|
if (value.name === '') {
|
|
desc = '() => {}';
|
|
} else {
|
|
desc = value.name + '() {}';
|
|
}
|
|
break;
|
|
case 'string':
|
|
if (value === OMITTED_PROP_ERROR) {
|
|
desc = '\u2026'; // ellipsis
|
|
} else {
|
|
desc = JSON.stringify(value);
|
|
}
|
|
break;
|
|
case 'undefined':
|
|
desc = 'undefined';
|
|
break;
|
|
case 'boolean':
|
|
desc = value ? 'true' : 'false';
|
|
break;
|
|
default:
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
desc = String(value);
|
|
}
|
|
properties.push([prefix + '\xa0\xa0'.repeat(indent) + propertyName, desc]);
|
|
}
|
|
|
|
const REMOVED = '\u2013\xa0';
|
|
const ADDED = '+\xa0';
|
|
const UNCHANGED = '\u2007\xa0';
|
|
|
|
export function addObjectDiffToProperties(
|
|
prev: Object,
|
|
next: Object,
|
|
properties: Array<[string, string]>,
|
|
indent: number,
|
|
): boolean {
|
|
// Note: We diff even non-owned properties here but things that are shared end up just the same.
|
|
// If a property is added or removed, we just emit the property name and omit the value it had.
|
|
// Mainly for performance. We need to minimize to only relevant information.
|
|
let isDeeplyEqual = true;
|
|
for (const key in prev) {
|
|
if (!(key in next)) {
|
|
properties.push([REMOVED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
|
|
isDeeplyEqual = false;
|
|
}
|
|
}
|
|
for (const key in next) {
|
|
if (key in prev) {
|
|
const prevValue = prev[key];
|
|
const nextValue = next[key];
|
|
if (prevValue !== nextValue) {
|
|
if (indent === 0 && key === 'children') {
|
|
// Omit any change inside the top level children prop since it's expected to change
|
|
// with any change to children of the component and their props will be logged
|
|
// elsewhere but still mark it as a cause of render.
|
|
const line = '\xa0\xa0'.repeat(indent) + key;
|
|
properties.push([REMOVED + line, '\u2026'], [ADDED + line, '\u2026']);
|
|
isDeeplyEqual = false;
|
|
continue;
|
|
}
|
|
if (indent >= 3) {
|
|
// Just fallthrough to print the two values if we're deep.
|
|
// This will skip nested properties of the objects.
|
|
} else if (
|
|
typeof prevValue === 'object' &&
|
|
typeof nextValue === 'object' &&
|
|
prevValue !== null &&
|
|
nextValue !== null &&
|
|
prevValue.$$typeof === nextValue.$$typeof
|
|
) {
|
|
if (nextValue.$$typeof === REACT_ELEMENT_TYPE) {
|
|
if (
|
|
prevValue.type === nextValue.type &&
|
|
prevValue.key === nextValue.key
|
|
) {
|
|
// If the only thing that has changed is the props of a nested element, then
|
|
// we omit the props because it is likely to be represented as a diff elsewhere.
|
|
const typeName =
|
|
getComponentNameFromType(nextValue.type) || '\u2026';
|
|
const line = '\xa0\xa0'.repeat(indent) + key;
|
|
const desc = '<' + typeName + ' \u2026 />';
|
|
properties.push([REMOVED + line, desc], [ADDED + line, desc]);
|
|
isDeeplyEqual = false;
|
|
continue;
|
|
}
|
|
} else {
|
|
// $FlowFixMe[method-unbinding]
|
|
const prevKind = Object.prototype.toString.call(prevValue);
|
|
// $FlowFixMe[method-unbinding]
|
|
const nextKind = Object.prototype.toString.call(nextValue);
|
|
if (
|
|
prevKind === nextKind &&
|
|
(nextKind === '[object Object]' || nextKind === '[object Array]')
|
|
) {
|
|
// Diff nested object
|
|
const entry = [
|
|
UNCHANGED + '\xa0\xa0'.repeat(indent) + key,
|
|
nextKind === '[object Array]' ? 'Array' : '',
|
|
];
|
|
properties.push(entry);
|
|
const prevLength = properties.length;
|
|
const nestedEqual = addObjectDiffToProperties(
|
|
prevValue,
|
|
nextValue,
|
|
properties,
|
|
indent + 1,
|
|
);
|
|
if (!nestedEqual) {
|
|
isDeeplyEqual = false;
|
|
} else if (prevLength === properties.length) {
|
|
// Nothing notably changed inside the nested object. So this is only a change in reference
|
|
// equality. Let's note it.
|
|
entry[1] =
|
|
'Referentially unequal but deeply equal objects. Consider memoization.';
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
} else if (
|
|
typeof prevValue === 'function' &&
|
|
typeof nextValue === 'function' &&
|
|
prevValue.name === nextValue.name &&
|
|
prevValue.length === nextValue.length
|
|
) {
|
|
// $FlowFixMe[method-unbinding]
|
|
const prevSrc = Function.prototype.toString.call(prevValue);
|
|
// $FlowFixMe[method-unbinding]
|
|
const nextSrc = Function.prototype.toString.call(nextValue);
|
|
if (prevSrc === nextSrc) {
|
|
// This looks like it might be the same function but different closures.
|
|
let desc;
|
|
if (nextValue.name === '') {
|
|
desc = '() => {}';
|
|
} else {
|
|
desc = nextValue.name + '() {}';
|
|
}
|
|
properties.push([
|
|
UNCHANGED + '\xa0\xa0'.repeat(indent) + key,
|
|
desc +
|
|
' Referentially unequal function closure. Consider memoization.',
|
|
]);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Otherwise, emit the change in property and the values.
|
|
addValueToProperties(key, prevValue, properties, indent, REMOVED);
|
|
addValueToProperties(key, nextValue, properties, indent, ADDED);
|
|
isDeeplyEqual = false;
|
|
}
|
|
} else {
|
|
properties.push([ADDED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
|
|
isDeeplyEqual = false;
|
|
}
|
|
}
|
|
return isDeeplyEqual;
|
|
}
|