/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/react/src/jsx/ReactJSXElement.js
840 строк
26 KB
Hendrik Liebau
[Flight] Fix wrong missing key warning when static child is blocked (#34350)
01 сен 2025, 12:03
Не верифицирован
01 сен 2025, 12:03
bb6f0c8
Код
Авторство
О чём код?
/** * 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. */ import getComponentNameFromType from 'shared/getComponentNameFromType'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import hasOwnProperty from 'shared/hasOwnProperty'; import assign from 'shared/assign'; import { REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE, REACT_LAZY_TYPE, } from 'shared/ReactSymbols'; import {checkKeyStringCoercion} from 'shared/CheckStringCoercion'; import isArray from 'shared/isArray'; import {ownerStackLimit} from 'shared/ReactFeatureFlags'; const createTask = // eslint-disable-next-line react-internal/no-production-logging __DEV__ && console.createTask ? // eslint-disable-next-line react-internal/no-production-logging console.createTask : () => null; function getTaskName(type) { if (type === REACT_FRAGMENT_TYPE) { return '<>'; } if ( typeof type === 'object' && type !== null && type.$$typeof === REACT_LAZY_TYPE ) { // We don't want to eagerly initialize the initializer in DEV mode so we can't // call it to extract the type so we don't know the type of this component. return '<...>'; } try { const name = getComponentNameFromType(type); return name ? '<' + name + '>' : '<...>'; } catch (x) { return '<...>'; } } function getOwner() { if (__DEV__) { const dispatcher = ReactSharedInternals.A; if (dispatcher === null) { return null; } return dispatcher.getOwner(); } return null; } /** @noinline */ function UnknownOwner() { /** @noinline */ return (() => Error('react-stack-top-frame'))(); } const createFakeCallStack = { react_stack_bottom_frame: function (callStackForError) { return callStackForError(); }, }; let specialPropKeyWarningShown; let didWarnAboutElementRef; let didWarnAboutOldJSXRuntime; let unknownOwnerDebugStack; let unknownOwnerDebugTask; if (__DEV__) { didWarnAboutElementRef = {}; // We use this technique to trick minifiers to preserve the function name. unknownOwnerDebugStack = createFakeCallStack.react_stack_bottom_frame.bind( createFakeCallStack, UnknownOwner, )(); unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner)); } function hasValidRef(config) { if (__DEV__) { if (hasOwnProperty.call(config, 'ref')) { const getter = Object.getOwnPropertyDescriptor(config, 'ref').get; if (getter && getter.isReactWarning) { return false; } } } return config.ref !== undefined; } function hasValidKey(config) { if (__DEV__) { if (hasOwnProperty.call(config, 'key')) { const getter = Object.getOwnPropertyDescriptor(config, 'key').get; if (getter && getter.isReactWarning) { return false; } } } return config.key !== undefined; } function defineKeyPropWarningGetter(props, displayName) { if (__DEV__) { const warnAboutAccessingKey = function () { if (!specialPropKeyWarningShown) { specialPropKeyWarningShown = true; console.error( '%s: `key` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + 'prop. (https://react.dev/link/special-props)', displayName, ); } }; warnAboutAccessingKey.isReactWarning = true; Object.defineProperty(props, 'key', { get: warnAboutAccessingKey, configurable: true, }); } } function elementRefGetterWithDeprecationWarning() { if (__DEV__) { const componentName = getComponentNameFromType(this.type); if (!didWarnAboutElementRef[componentName]) { didWarnAboutElementRef[componentName] = true; console.error( 'Accessing element.ref was removed in React 19. ref is now a ' + 'regular prop. It will be removed from the JSX Element ' + 'type in a future release.', ); } // An undefined `element.ref` is coerced to `null` for // backwards compatibility. const refProp = this.props.ref; return refProp !== undefined ? refProp : null; } } /** * Factory method to create a new React element. This no longer adheres to * the class pattern, so do not use new to call it. Also, instanceof check * will not work. Instead test $$typeof field against Symbol.for('react.transitional.element') to check * if something is a React Element. * * @internal */ function ReactElement(type, key, props, owner, debugStack, debugTask) { // Ignore whatever was passed as the ref argument and treat `props.ref` as // the source of truth. The only thing we use this for is `element.ref`, // which will log a deprecation warning on access. In the next release, we // can remove `element.ref` as well as the `ref` argument. const refProp = props.ref; // An undefined `element.ref` is coerced to `null` for // backwards compatibility. const ref = refProp !== undefined ? refProp : null; let element; if (__DEV__) { // In dev, make `ref` a non-enumerable property with a warning. It's non- // enumerable so that test matchers and serializers don't access it and // trigger the warning. // // `ref` will be removed from the element completely in a future release. element = { // This tag allows us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, // Built-in properties that belong on the element type, key, props, // Record the component responsible for creating this element. _owner: owner, }; if (ref !== null) { Object.defineProperty(element, 'ref', { enumerable: false, get: elementRefGetterWithDeprecationWarning, }); } else { // Don't warn on access if a ref is not given. This reduces false // positives in cases where a test serializer uses // getOwnPropertyDescriptors to compare objects, like Jest does, which is // a problem because it bypasses non-enumerability. // // So unfortunately this will trigger a false positive warning in Jest // when the diff is printed: // // expect(<div ref={ref} />).toEqual(<span ref={ref} />); // // A bit sketchy, but this is what we've done for the `props.key` and // `props.ref` accessors for years, which implies it will be good enough // for `element.ref`, too. Let's see if anyone complains. Object.defineProperty(element, 'ref', { enumerable: false, value: null, }); } } else { // In prod, `ref` is a regular property and _owner doesn't exist. element = { // This tag allows us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, // Built-in properties that belong on the element type, key, ref, props, }; } if (__DEV__) { // The validation flag is currently mutative. We put it on // an external backing store so that we can freeze the whole object. // This can be replaced with a WeakMap once they are implemented in // commonly used development environments. element._store = {}; // To make comparing ReactElements easier for testing purposes, we make // the validation flag non-enumerable (where possible, which should // include every environment we run tests in), so the test framework // ignores it. Object.defineProperty(element._store, 'validated', { configurable: false, enumerable: false, writable: true, value: 0, }); // debugInfo contains Server Component debug information. Object.defineProperty(element, '_debugInfo', { configurable: false, enumerable: false, writable: true, value: null, }); Object.defineProperty(element, '_debugStack', { configurable: false, enumerable: false, writable: true, value: debugStack, }); Object.defineProperty(element, '_debugTask', { configurable: false, enumerable: false, writable: true, value: debugTask, }); if (Object.freeze) { Object.freeze(element.props); Object.freeze(element); } } return element; } /** * https://github.com/reactjs/rfcs/pull/107 * @param {*} type * @param {object} props * @param {string} key */ export function jsxProd(type, config, maybeKey) { let key = null; // Currently, key can be spread in as a prop. This causes a potential // issue if key is also explicitly declared (ie. <div {...props} key="Hi" /> // or <div key="Hi" {...props} /> ). We want to deprecate key spread, // but as an intermediary step, we will use jsxDEV for everything except // <div {...props} key="Hi" />, because we aren't currently able to tell if // key is explicitly declared to be undefined or not. if (maybeKey !== undefined) { if (__DEV__) { checkKeyStringCoercion(maybeKey); } key = '' + maybeKey; } if (hasValidKey(config)) { if (__DEV__) { checkKeyStringCoercion(config.key); } key = '' + config.key; } let props; if (!('key' in config)) { // If key was not spread in, we can reuse the original props object. This // only works for `jsx`, not `createElement`, because `jsx` is a compiler // target and the compiler always passes a new object. For `createElement`, // we can't assume a new object is passed every time because it can be // called manually. // // Spreading key is a warning in dev. In a future release, we will not // remove a spread key from the props object. (But we'll still warn.) We'll // always pass the object straight through. props = config; } else { // We need to remove reserved props (key, prop, ref). Create a fresh props // object and copy over all the non-reserved props. We don't use `delete` // because in V8 it will deopt the object to dictionary mode. props = {}; for (const propName in config) { // Skip over reserved prop names if (propName !== 'key') { props[propName] = config[propName]; } } } return ReactElement(type, key, props, getOwner(), undefined, undefined); } // While `jsxDEV` should never be called when running in production, we do // support `jsx` and `jsxs` when running in development. This supports the case // where a third-party dependency ships code that was compiled for production; // we want to still provide warnings in development. // // So these functions are the _dev_ implementations of the _production_ // API signatures. // // Since these functions are dev-only, it's ok to add an indirection here. They // only exist to provide different versions of `isStaticChildren`. (We shouldn't // use this pattern for the prod versions, though, because it will add an call // frame.) export function jsxProdSignatureRunningInDevWithDynamicChildren( type, config, maybeKey, ) { if (__DEV__) { const isStaticChildren = false; const trackActualOwner = __DEV__ && ReactSharedInternals.recentlyCreatedOwnerStacks++ < ownerStackLimit; return jsxDEVImpl( type, config, maybeKey, isStaticChildren, __DEV__ && (trackActualOwner ? Error('react-stack-top-frame') : unknownOwnerDebugStack), __DEV__ && (trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask), ); } } export function jsxProdSignatureRunningInDevWithStaticChildren( type, config, maybeKey, ) { if (__DEV__) { const isStaticChildren = true; const trackActualOwner = __DEV__ && ReactSharedInternals.recentlyCreatedOwnerStacks++ < ownerStackLimit; return jsxDEVImpl( type, config, maybeKey, isStaticChildren, __DEV__ && (trackActualOwner ? Error('react-stack-top-frame') : unknownOwnerDebugStack), __DEV__ && (trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask), ); } } const didWarnAboutKeySpread = {}; /** * https://github.com/reactjs/rfcs/pull/107 * @param {*} type * @param {object} props * @param {string} key */ export function jsxDEV(type, config, maybeKey, isStaticChildren) { const trackActualOwner = __DEV__ && ReactSharedInternals.recentlyCreatedOwnerStacks++ < ownerStackLimit; return jsxDEVImpl( type, config, maybeKey, isStaticChildren, __DEV__ && (trackActualOwner ? Error('react-stack-top-frame') : unknownOwnerDebugStack), __DEV__ && (trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask), ); } function jsxDEVImpl( type, config, maybeKey, isStaticChildren, debugStack, debugTask, ) { if (__DEV__) { // We don't warn for invalid element type here because with owner stacks, // we error in the renderer. The renderer is the only one that knows what // types are valid for this particular renderer so we let it error there. // Skip key warning if the type isn't valid since our key validation logic // doesn't expect a non-string/function type and can throw confusing // errors. We don't want exception behavior to differ between dev and // prod. (Rendering will throw with a helpful message and as soon as the // type is fixed, the key warnings will appear.) // With owner stacks, we no longer need the type here so this comment is // no longer true. Which is why we can run this even for invalid types. const children = config.children; if (children !== undefined) { if (isStaticChildren) { if (isArray(children)) { for (let i = 0; i < children.length; i++) { validateChildKeys(children[i]); } if (Object.freeze) { Object.freeze(children); } } else { console.error( 'React.jsx: Static children should always be an array. ' + 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' + 'Use the Babel transform instead.', ); } } else { validateChildKeys(children); } } // Warn about key spread regardless of whether the type is valid. if (hasOwnProperty.call(config, 'key')) { const componentName = getComponentNameFromType(type); const keys = Object.keys(config).filter(k => k !== 'key'); const beforeExample = keys.length > 0 ? '{key: someKey, ' + keys.join(': ..., ') + ': ...}' : '{key: someKey}'; if (!didWarnAboutKeySpread[componentName + beforeExample]) { const afterExample = keys.length > 0 ? '{' + keys.join(': ..., ') + ': ...}' : '{}'; console.error( 'A props object containing a "key" prop is being spread into JSX:\n' + ' let props = %s;\n' + ' <%s {...props} />\n' + 'React keys must be passed directly to JSX without using spread:\n' + ' let props = %s;\n' + ' <%s key={someKey} {...props} />', beforeExample, componentName, afterExample, componentName, ); didWarnAboutKeySpread[componentName + beforeExample] = true; } } let key = null; // Currently, key can be spread in as a prop. This causes a potential // issue if key is also explicitly declared (ie. <div {...props} key="Hi" /> // or <div key="Hi" {...props} /> ). We want to deprecate key spread, // but as an intermediary step, we will use jsxDEV for everything except // <div {...props} key="Hi" />, because we aren't currently able to tell if // key is explicitly declared to be undefined or not. if (maybeKey !== undefined) { if (__DEV__) { checkKeyStringCoercion(maybeKey); } key = '' + maybeKey; } if (hasValidKey(config)) { if (__DEV__) { checkKeyStringCoercion(config.key); } key = '' + config.key; } let props; if (!('key' in config)) { // If key was not spread in, we can reuse the original props object. This // only works for `jsx`, not `createElement`, because `jsx` is a compiler // target and the compiler always passes a new object. For `createElement`, // we can't assume a new object is passed every time because it can be // called manually. // // Spreading key is a warning in dev. In a future release, we will not // remove a spread key from the props object. (But we'll still warn.) We'll // always pass the object straight through. props = config; } else { // We need to remove reserved props (key, prop, ref). Create a fresh props // object and copy over all the non-reserved props. We don't use `delete` // because in V8 it will deopt the object to dictionary mode. props = {}; for (const propName in config) { // Skip over reserved prop names if (propName !== 'key') { props[propName] = config[propName]; } } } if (key) { const displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type; defineKeyPropWarningGetter(props, displayName); } return ReactElement(type, key, props, getOwner(), debugStack, debugTask); } } /** * Create and return a new ReactElement of the given type. * See https://reactjs.org/docs/react-api.html#createelement */ export function createElement(type, config, children) { if (__DEV__) { // We don't warn for invalid element type here because with owner stacks, // we error in the renderer. The renderer is the only one that knows what // types are valid for this particular renderer so we let it error there. // Skip key warning if the type isn't valid since our key validation logic // doesn't expect a non-string/function type and can throw confusing // errors. We don't want exception behavior to differ between dev and // prod. (Rendering will throw with a helpful message and as soon as the // type is fixed, the key warnings will appear.) for (let i = 2; i < arguments.length; i++) { validateChildKeys(arguments[i]); } // Unlike the jsx() runtime, createElement() doesn't warn about key spread. } let propName; // Reserved names are extracted const props = {}; let key = null; if (config != null) { if (__DEV__) { if ( !didWarnAboutOldJSXRuntime && '__self' in config && // Do not assume this is the result of an oudated JSX transform if key // is present, because the modern JSX transform sometimes outputs // createElement to preserve precedence between a static key and a // spread key. To avoid false positive warnings, we never warn if // there's a key. !('key' in config) ) { didWarnAboutOldJSXRuntime = true; console.warn( 'Your app (or one of its dependencies) is using an outdated JSX ' + 'transform. Update to the modern JSX transform for ' + 'faster performance: https://react.dev/link/new-jsx-transform', ); } } if (hasValidKey(config)) { if (__DEV__) { checkKeyStringCoercion(config.key); } key = '' + config.key; } // Remaining properties are added to a new props object for (propName in config) { if ( hasOwnProperty.call(config, propName) && // Skip over reserved prop names propName !== 'key' && // Even though we don't use these anymore in the runtime, we don't want // them to appear as props, so in createElement we filter them out. // We don't have to do this in the jsx() runtime because the jsx() // transform never passed these as props; it used separate arguments. propName !== '__self' && propName !== '__source' ) { props[propName] = config[propName]; } } } // Children can be more than one argument, and those are transferred onto // the newly allocated props object. const childrenLength = arguments.length - 2; if (childrenLength === 1) { props.children = children; } else if (childrenLength > 1) { const childArray = Array(childrenLength); for (let i = 0; i < childrenLength; i++) { childArray[i] = arguments[i + 2]; } if (__DEV__) { if (Object.freeze) { Object.freeze(childArray); } } props.children = childArray; } // Resolve default props if (type && type.defaultProps) { const defaultProps = type.defaultProps; for (propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } } if (__DEV__) { if (key) { const displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type; defineKeyPropWarningGetter(props, displayName); } } const trackActualOwner = __DEV__ && ReactSharedInternals.recentlyCreatedOwnerStacks++ < ownerStackLimit; return ReactElement( type, key, props, getOwner(), __DEV__ && (trackActualOwner ? Error('react-stack-top-frame') : unknownOwnerDebugStack), __DEV__ && (trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask), ); } export function cloneAndReplaceKey(oldElement, newKey) { const clonedElement = ReactElement( oldElement.type, newKey, oldElement.props, !__DEV__ ? undefined : oldElement._owner, __DEV__ && oldElement._debugStack, __DEV__ && oldElement._debugTask, ); if (__DEV__) { // The cloned element should inherit the original element's key validation. if (oldElement._store) { clonedElement._store.validated = oldElement._store.validated; } } return clonedElement; } /** * Clone and return a new ReactElement using element as the starting point. * See https://reactjs.org/docs/react-api.html#cloneelement */ export function cloneElement(element, config, children) { if (element === null || element === undefined) { throw new Error( `The argument must be a React element, but you passed ${element}.`, ); } let propName; // Original props are copied const props = assign({}, element.props); // Reserved names are extracted let key = element.key; // Owner will be preserved, unless ref is overridden let owner = !__DEV__ ? undefined : element._owner; if (config != null) { if (hasValidRef(config)) { owner = __DEV__ ? getOwner() : undefined; } if (hasValidKey(config)) { if (__DEV__) { checkKeyStringCoercion(config.key); } key = '' + config.key; } // Remaining properties override existing props for (propName in config) { if ( hasOwnProperty.call(config, propName) && // Skip over reserved prop names propName !== 'key' && // ...and maybe these, too, though we currently rely on them for // warnings and debug information in dev. Need to decide if we're OK // with dropping them. In the jsx() runtime it's not an issue because // the data gets passed as separate arguments instead of props, but // it would be nice to stop relying on them entirely so we can drop // them from the internal Fiber field. propName !== '__self' && propName !== '__source' && // Undefined `ref` is ignored by cloneElement. We treat it the same as // if the property were missing. This is mostly for // backwards compatibility. !(propName === 'ref' && config.ref === undefined) ) { props[propName] = config[propName]; } } } // Children can be more than one argument, and those are transferred onto // the newly allocated props object. const childrenLength = arguments.length - 2; if (childrenLength === 1) { props.children = children; } else if (childrenLength > 1) { const childArray = Array(childrenLength); for (let i = 0; i < childrenLength; i++) { childArray[i] = arguments[i + 2]; } props.children = childArray; } const clonedElement = ReactElement( element.type, key, props, owner, __DEV__ && element._debugStack, __DEV__ && element._debugTask, ); for (let i = 2; i < arguments.length; i++) { validateChildKeys(arguments[i]); } return clonedElement; } /** * Ensure that every element either is passed in a static location, in an * array with an explicit keys property defined, or in an object literal * with valid key property. * * @internal * @param {ReactNode} node Statically passed child of any type. * @param {*} parentType node's parent's type. */ function validateChildKeys(node) { if (__DEV__) { // Mark elements as being in a valid static child position so they // don't need keys. if (isValidElement(node)) { if (node._store) { node._store.validated = 1; } } else if (isLazyType(node)) { if (node._payload.status === 'fulfilled') { if (isValidElement(node._payload.value) && node._payload.value._store) { node._payload.value._store.validated = 1; } } else if (node._store) { node._store.validated = 1; } } } } /** * Verifies the object is a ReactElement. * See https://reactjs.org/docs/react-api.html#isvalidelement * @param {?object} object * @return {boolean} True if `object` is a ReactElement. * @final */ export function isValidElement(object) { return ( typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE ); } export function isLazyType(object) { return ( typeof object === 'object' && object !== null && object.$$typeof === REACT_LAZY_TYPE ); }