mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Depends on: - #28317 - #28320 --- Changes the behavior of the JSX runtime to pass through `ref` as a normal prop, rather than plucking it from the props object and storing on the element. This is a breaking change since it changes the type of the receiving component. However, most code is unaffected since it's unlikely that a component would have attempted to access a `ref` prop, since it was not possible to get a reference to one. `forwardRef` _will_ still pluck `ref` from the props object, though, since it's extremely common for users to spread the props object onto the inner component and pass `ref` as a differently named prop. This is for maximum compatibility with existing code — the real impact of this change is that `forwardRef` is no longer required. Currently, refs are resolved during child reconciliation and stored on the fiber. As a result of this change, we can move ref resolution to happen only much later, and only for components that actually use them. Then we can remove the `ref` field from the Fiber type. I have not yet done that in this step, though.
381 lines
12 KiB
JavaScript
381 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.
|
|
*/
|
|
|
|
import {ATTRIBUTE_NAME_CHAR} from './isAttributeNameSafe';
|
|
import isCustomElement from './isCustomElement';
|
|
import possibleStandardNames from './possibleStandardNames';
|
|
import hasOwnProperty from 'shared/hasOwnProperty';
|
|
import {
|
|
enableCustomElementPropertySupport,
|
|
enableFormActions,
|
|
} from 'shared/ReactFeatureFlags';
|
|
|
|
const warnedProperties = {};
|
|
const EVENT_NAME_REGEX = /^on./;
|
|
const INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/;
|
|
const rARIA = __DEV__
|
|
? new RegExp('^(aria)-[' + ATTRIBUTE_NAME_CHAR + ']*$')
|
|
: null;
|
|
const rARIACamel = __DEV__
|
|
? new RegExp('^(aria)[A-Z][' + ATTRIBUTE_NAME_CHAR + ']*$')
|
|
: null;
|
|
|
|
function validateProperty(tagName, name, value, eventRegistry) {
|
|
if (__DEV__) {
|
|
if (hasOwnProperty.call(warnedProperties, name) && warnedProperties[name]) {
|
|
return true;
|
|
}
|
|
|
|
const lowerCasedName = name.toLowerCase();
|
|
if (lowerCasedName === 'onfocusin' || lowerCasedName === 'onfocusout') {
|
|
console.error(
|
|
'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
|
|
'All React events are normalized to bubble, so onFocusIn and onFocusOut ' +
|
|
'are not needed/supported by React.',
|
|
);
|
|
warnedProperties[name] = true;
|
|
return true;
|
|
}
|
|
|
|
if (enableFormActions) {
|
|
// Actions are special because unlike events they can have other value types.
|
|
if (typeof value === 'function') {
|
|
if (tagName === 'form' && name === 'action') {
|
|
return true;
|
|
}
|
|
if (tagName === 'input' && name === 'formAction') {
|
|
return true;
|
|
}
|
|
if (tagName === 'button' && name === 'formAction') {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// We can't rely on the event system being injected on the server.
|
|
if (eventRegistry != null) {
|
|
const {registrationNameDependencies, possibleRegistrationNames} =
|
|
eventRegistry;
|
|
if (registrationNameDependencies.hasOwnProperty(name)) {
|
|
return true;
|
|
}
|
|
const registrationName = possibleRegistrationNames.hasOwnProperty(
|
|
lowerCasedName,
|
|
)
|
|
? possibleRegistrationNames[lowerCasedName]
|
|
: null;
|
|
if (registrationName != null) {
|
|
console.error(
|
|
'Invalid event handler property `%s`. Did you mean `%s`?',
|
|
name,
|
|
registrationName,
|
|
);
|
|
warnedProperties[name] = true;
|
|
return true;
|
|
}
|
|
if (EVENT_NAME_REGEX.test(name)) {
|
|
console.error(
|
|
'Unknown event handler property `%s`. It will be ignored.',
|
|
name,
|
|
);
|
|
warnedProperties[name] = true;
|
|
return true;
|
|
}
|
|
} else if (EVENT_NAME_REGEX.test(name)) {
|
|
// If no event plugins have been injected, we are in a server environment.
|
|
// So we can't tell if the event name is correct for sure, but we can filter
|
|
// out known bad ones like `onclick`. We can't suggest a specific replacement though.
|
|
if (INVALID_EVENT_NAME_REGEX.test(name)) {
|
|
console.error(
|
|
'Invalid event handler property `%s`. ' +
|
|
'React events use the camelCase naming convention, for example `onClick`.',
|
|
name,
|
|
);
|
|
}
|
|
warnedProperties[name] = true;
|
|
return true;
|
|
}
|
|
|
|
// Let the ARIA attribute hook validate ARIA attributes
|
|
if (rARIA.test(name) || rARIACamel.test(name)) {
|
|
return true;
|
|
}
|
|
|
|
if (lowerCasedName === 'innerhtml') {
|
|
console.error(
|
|
'Directly setting property `innerHTML` is not permitted. ' +
|
|
'For more information, lookup documentation on `dangerouslySetInnerHTML`.',
|
|
);
|
|
warnedProperties[name] = true;
|
|
return true;
|
|
}
|
|
|
|
if (lowerCasedName === 'aria') {
|
|
console.error(
|
|
'The `aria` attribute is reserved for future use in React. ' +
|
|
'Pass individual `aria-` attributes instead.',
|
|
);
|
|
warnedProperties[name] = true;
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
lowerCasedName === 'is' &&
|
|
value !== null &&
|
|
value !== undefined &&
|
|
typeof value !== 'string'
|
|
) {
|
|
console.error(
|
|
'Received a `%s` for a string attribute `is`. If this is expected, cast ' +
|
|
'the value to a string.',
|
|
typeof value,
|
|
);
|
|
warnedProperties[name] = true;
|
|
return true;
|
|
}
|
|
|
|
if (typeof value === 'number' && isNaN(value)) {
|
|
console.error(
|
|
'Received NaN for the `%s` attribute. If this is expected, cast ' +
|
|
'the value to a string.',
|
|
name,
|
|
);
|
|
warnedProperties[name] = true;
|
|
return true;
|
|
}
|
|
|
|
// Known attributes should match the casing specified in the property config.
|
|
if (possibleStandardNames.hasOwnProperty(lowerCasedName)) {
|
|
const standardName = possibleStandardNames[lowerCasedName];
|
|
if (standardName !== name) {
|
|
console.error(
|
|
'Invalid DOM property `%s`. Did you mean `%s`?',
|
|
name,
|
|
standardName,
|
|
);
|
|
warnedProperties[name] = true;
|
|
return true;
|
|
}
|
|
} else if (name !== lowerCasedName) {
|
|
// Unknown attributes should have lowercase casing since that's how they
|
|
// will be cased anyway with server rendering.
|
|
console.error(
|
|
'React does not recognize the `%s` prop on a DOM element. If you ' +
|
|
'intentionally want it to appear in the DOM as a custom ' +
|
|
'attribute, spell it as lowercase `%s` instead. ' +
|
|
'If you accidentally passed it from a parent component, remove ' +
|
|
'it from the DOM element.',
|
|
name,
|
|
lowerCasedName,
|
|
);
|
|
warnedProperties[name] = true;
|
|
return true;
|
|
}
|
|
|
|
// Now that we've validated casing, do not validate
|
|
// data types for reserved props
|
|
switch (name) {
|
|
case 'dangerouslySetInnerHTML':
|
|
case 'children':
|
|
case 'style':
|
|
case 'suppressContentEditableWarning':
|
|
case 'suppressHydrationWarning':
|
|
case 'defaultValue': // Reserved
|
|
case 'defaultChecked':
|
|
case 'innerHTML':
|
|
case 'ref': {
|
|
return true;
|
|
}
|
|
case 'innerText': // Properties
|
|
case 'textContent':
|
|
if (enableCustomElementPropertySupport) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
switch (typeof value) {
|
|
case 'boolean': {
|
|
switch (name) {
|
|
case 'autoFocus':
|
|
case 'checked':
|
|
case 'multiple':
|
|
case 'muted':
|
|
case 'selected':
|
|
case 'contentEditable':
|
|
case 'spellCheck':
|
|
case 'draggable':
|
|
case 'value':
|
|
case 'autoReverse':
|
|
case 'externalResourcesRequired':
|
|
case 'focusable':
|
|
case 'preserveAlpha':
|
|
case 'allowFullScreen':
|
|
case 'async':
|
|
case 'autoPlay':
|
|
case 'controls':
|
|
case 'default':
|
|
case 'defer':
|
|
case 'disabled':
|
|
case 'disablePictureInPicture':
|
|
case 'disableRemotePlayback':
|
|
case 'formNoValidate':
|
|
case 'hidden':
|
|
case 'loop':
|
|
case 'noModule':
|
|
case 'noValidate':
|
|
case 'open':
|
|
case 'playsInline':
|
|
case 'readOnly':
|
|
case 'required':
|
|
case 'reversed':
|
|
case 'scoped':
|
|
case 'seamless':
|
|
case 'itemScope':
|
|
case 'capture':
|
|
case 'download': {
|
|
// Boolean properties can accept boolean values
|
|
return true;
|
|
}
|
|
default: {
|
|
const prefix = name.toLowerCase().slice(0, 5);
|
|
if (prefix === 'data-' || prefix === 'aria-') {
|
|
return true;
|
|
}
|
|
if (value) {
|
|
console.error(
|
|
'Received `%s` for a non-boolean attribute `%s`.\n\n' +
|
|
'If you want to write it to the DOM, pass a string instead: ' +
|
|
'%s="%s" or %s={value.toString()}.',
|
|
value,
|
|
name,
|
|
name,
|
|
value,
|
|
name,
|
|
);
|
|
} else {
|
|
console.error(
|
|
'Received `%s` for a non-boolean attribute `%s`.\n\n' +
|
|
'If you want to write it to the DOM, pass a string instead: ' +
|
|
'%s="%s" or %s={value.toString()}.\n\n' +
|
|
'If you used to conditionally omit it with %s={condition && value}, ' +
|
|
'pass %s={condition ? value : undefined} instead.',
|
|
value,
|
|
name,
|
|
name,
|
|
value,
|
|
name,
|
|
name,
|
|
name,
|
|
);
|
|
}
|
|
warnedProperties[name] = true;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
case 'function':
|
|
case 'symbol': // eslint-disable-line
|
|
// Warn when a known attribute is a bad type
|
|
warnedProperties[name] = true;
|
|
return false;
|
|
case 'string': {
|
|
// Warn when passing the strings 'false' or 'true' into a boolean prop
|
|
if (value === 'false' || value === 'true') {
|
|
switch (name) {
|
|
case 'checked':
|
|
case 'selected':
|
|
case 'multiple':
|
|
case 'muted':
|
|
case 'allowFullScreen':
|
|
case 'async':
|
|
case 'autoPlay':
|
|
case 'controls':
|
|
case 'default':
|
|
case 'defer':
|
|
case 'disabled':
|
|
case 'disablePictureInPicture':
|
|
case 'disableRemotePlayback':
|
|
case 'formNoValidate':
|
|
case 'hidden':
|
|
case 'loop':
|
|
case 'noModule':
|
|
case 'noValidate':
|
|
case 'open':
|
|
case 'playsInline':
|
|
case 'readOnly':
|
|
case 'required':
|
|
case 'reversed':
|
|
case 'scoped':
|
|
case 'seamless':
|
|
case 'itemScope': {
|
|
break;
|
|
}
|
|
default: {
|
|
return true;
|
|
}
|
|
}
|
|
console.error(
|
|
'Received the string `%s` for the boolean attribute `%s`. ' +
|
|
'%s ' +
|
|
'Did you mean %s={%s}?',
|
|
value,
|
|
name,
|
|
value === 'false'
|
|
? 'The browser will interpret it as a truthy value.'
|
|
: 'Although this works, it will not work as expected if you pass the string "false".',
|
|
name,
|
|
value,
|
|
);
|
|
warnedProperties[name] = true;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function warnUnknownProperties(type, props, eventRegistry) {
|
|
if (__DEV__) {
|
|
const unknownProps = [];
|
|
for (const key in props) {
|
|
const isValid = validateProperty(type, key, props[key], eventRegistry);
|
|
if (!isValid) {
|
|
unknownProps.push(key);
|
|
}
|
|
}
|
|
|
|
const unknownPropString = unknownProps
|
|
.map(prop => '`' + prop + '`')
|
|
.join(', ');
|
|
if (unknownProps.length === 1) {
|
|
console.error(
|
|
'Invalid value for prop %s on <%s> tag. Either remove it from the element, ' +
|
|
'or pass a string or number value to keep it in the DOM. ' +
|
|
'For details, see https://reactjs.org/link/attribute-behavior ',
|
|
unknownPropString,
|
|
type,
|
|
);
|
|
} else if (unknownProps.length > 1) {
|
|
console.error(
|
|
'Invalid values for props %s on <%s> tag. Either remove them from the element, ' +
|
|
'or pass a string or number value to keep them in the DOM. ' +
|
|
'For details, see https://reactjs.org/link/attribute-behavior ',
|
|
unknownPropString,
|
|
type,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function validateProperties(type, props, eventRegistry) {
|
|
if (isCustomElement(type, props) || typeof props.is === 'string') {
|
|
return;
|
|
}
|
|
warnUnknownProperties(type, props, eventRegistry);
|
|
}
|