From c15579631ff4d387401d57d9006d849ca1d5cd71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 4 Apr 2023 18:06:09 -0400 Subject: [PATCH] Put common aliases in Map/Set instead of switch over strings (#26551) This is a follow up to https://github.com/facebook/react/pull/26546 This is strictly a perf optimization since we know that switches over strings aren't optimally implemented in current engines. Basically they're a sequence of ifs. As a result, we're better off putting the unusual cases in a Map and the very common cases in the beginning of the switch. We might be better off putting very common cases in explicit ifs - just in case the engine does optimize switches to a hash table which is potentially worse. --------- Co-authored-by: Sophie Alpert --- .../src/client/DOMPropertyOperations.js | 27 + .../src/client/ReactDOMComponent.js | 1333 +++-------------- .../src/server/ReactDOMServerFormatConfig.js | 333 +--- .../src/shared/getAttributeAlias.js | 96 ++ .../src/shared/isUnitlessNumber.js | 150 +- 5 files changed, 492 insertions(+), 1447 deletions(-) create mode 100644 packages/react-dom-bindings/src/shared/getAttributeAlias.js diff --git a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js index 4066055e0197..ac311c72e01b 100644 --- a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js +++ b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js @@ -140,6 +140,33 @@ export function setValueForAttribute( } } +export function setValueForKnownAttribute( + node: Element, + name: string, + value: mixed, +) { + if (value === null) { + node.removeAttribute(name); + return; + } + switch (typeof value) { + case 'undefined': + case 'function': + case 'symbol': + case 'boolean': { + node.removeAttribute(name); + return; + } + } + if (__DEV__) { + checkAttributeStringCoercion(value, name); + } + node.setAttribute( + name, + enableTrustedTypesIntegration ? (value: any) : '' + (value: any), + ); +} + export function setValueForNamespacedAttribute( node: Element, namespace: string, diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index 60776996e537..20395d09ac09 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -22,6 +22,7 @@ import { getValueForAttribute, getValueForAttributeOnCustomComponent, setValueForPropertyOnCustomComponent, + setValueForKnownAttribute, setValueForAttribute, setValueForNamespacedAttribute, } from './DOMPropertyOperations'; @@ -58,6 +59,7 @@ import { } from './CSSPropertyOperations'; import {HTML_NAMESPACE, getIntrinsicNamespace} from './DOMNamespaces'; import isCustomElement from '../shared/isCustomElement'; +import getAttributeAlias from '../shared/getAttributeAlias'; import possibleStandardNames from '../shared/possibleStandardNames'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook'; @@ -275,35 +277,6 @@ function setProp( props: any, ): void { switch (key) { - case 'style': { - setValueForStyles(domElement, value); - break; - } - case 'dangerouslySetInnerHTML': { - if (value != null) { - if (typeof value !== 'object' || !('__html' in value)) { - throw new Error( - '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + - 'Please visit https://reactjs.org/link/dangerously-set-inner-html ' + - 'for more information.', - ); - } - const nextHtml: any = value.__html; - if (nextHtml != null) { - if (props.children != null) { - throw new Error( - 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.', - ); - } - if (disableIEWorkarounds) { - domElement.innerHTML = nextHtml; - } else { - setInnerHTML(domElement, nextHtml); - } - } - } - break; - } case 'children': { if (typeof value === 'string') { // Avoid setting initial textContent when the text is empty. In IE11 setting @@ -324,50 +297,26 @@ function setProp( } break; } - case 'onScroll': { - if (value != null) { - if (__DEV__ && typeof value !== 'function') { - warnForInvalidEventListener(key, value); - } - listenToNonDelegatedEvent('scroll', domElement); - } - break; - } - case 'onClick': { - // TODO: This cast may not be sound for SVG, MathML or custom elements. - if (value != null) { - if (__DEV__ && typeof value !== 'function') { - warnForInvalidEventListener(key, value); - } - trapClickOnNonInteractiveElement(((domElement: any): HTMLElement)); - } - break; - } - // Note: `option.selected` is not updated if `select.multiple` is - // disabled with `removeAttribute`. We have special logic for handling this. - case 'multiple': { - (domElement: any).multiple = - value && typeof value !== 'function' && typeof value !== 'symbol'; + // These are very common props and therefore are in the beginning of the switch. + // TODO: aria-label is a very common prop but allows booleans so is not like the others + // but should ideally go in this list too. + case 'className': + setValueForKnownAttribute(domElement, 'class', value); break; - } - case 'muted': { - (domElement: any).muted = - value && typeof value !== 'function' && typeof value !== 'symbol'; + case 'tabIndex': + // This has to be case sensitive in SVG. + setValueForKnownAttribute(domElement, 'tabindex', value); break; - } - case 'suppressContentEditableWarning': - case 'suppressHydrationWarning': - case 'defaultValue': // Reserved - case 'defaultChecked': - case 'innerHTML': { - // Noop + case 'dir': + case 'role': + case 'viewBox': + case 'width': + case 'height': { + setValueForKnownAttribute(domElement, key, value); break; } - case 'autoFocus': { - // We polyfill it separately on the client during commit. - // We could have excluded it in the property list instead of - // adding a special case here, but then it wouldn't be emitted - // on server rendering (but we *do* want to emit it in SSR). + case 'style': { + setValueForStyles(domElement, value); break; } // These attributes accept URLs. These must not allow javascript: URLS. @@ -423,6 +372,77 @@ function setProp( domElement.setAttribute(key, sanitizedValue); break; } + case 'onClick': { + // TODO: This cast may not be sound for SVG, MathML or custom elements. + if (value != null) { + if (__DEV__ && typeof value !== 'function') { + warnForInvalidEventListener(key, value); + } + trapClickOnNonInteractiveElement(((domElement: any): HTMLElement)); + } + break; + } + case 'onScroll': { + if (value != null) { + if (__DEV__ && typeof value !== 'function') { + warnForInvalidEventListener(key, value); + } + listenToNonDelegatedEvent('scroll', domElement); + } + break; + } + case 'dangerouslySetInnerHTML': { + if (value != null) { + if (typeof value !== 'object' || !('__html' in value)) { + throw new Error( + '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + + 'Please visit https://reactjs.org/link/dangerously-set-inner-html ' + + 'for more information.', + ); + } + const nextHtml: any = value.__html; + if (nextHtml != null) { + if (props.children != null) { + throw new Error( + 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.', + ); + } + if (disableIEWorkarounds) { + domElement.innerHTML = nextHtml; + } else { + setInnerHTML(domElement, nextHtml); + } + } + } + break; + } + // Note: `option.selected` is not updated if `select.multiple` is + // disabled with `removeAttribute`. We have special logic for handling this. + case 'multiple': { + (domElement: any).multiple = + value && typeof value !== 'function' && typeof value !== 'symbol'; + break; + } + case 'muted': { + (domElement: any).muted = + value && typeof value !== 'function' && typeof value !== 'symbol'; + break; + } + case 'suppressContentEditableWarning': + case 'suppressHydrationWarning': + case 'defaultValue': // Reserved + case 'defaultChecked': + case 'innerHTML': { + // Noop + break; + } + case 'autoFocus': { + // We polyfill it separately on the client during commit. + // We could have excluded it in the property list instead of + // adding a special case here, but then it wouldn't be emitted + // on server rendering (but we *do* want to emit it in SSR). + break; + } case 'xlinkHref': { if ( value == null || @@ -565,251 +585,6 @@ function setProp( } break; } - // A few React string attributes have a different name. - // This is a mapping from React prop names to the attribute names. - case 'acceptCharset': - setValueForAttribute(domElement, 'accept-charset', value); - break; - case 'className': - setValueForAttribute(domElement, 'class', value); - break; - case 'htmlFor': - setValueForAttribute(domElement, 'for', value); - break; - case 'httpEquiv': - setValueForAttribute(domElement, 'http-equiv', value); - break; - // HTML and SVG attributes, but the SVG attribute is case sensitive. - case 'tabIndex': - setValueForAttribute(domElement, 'tabindex', value); - break; - case 'crossOrigin': - setValueForAttribute(domElement, 'crossorigin', value); - break; - // This is a list of all SVG attributes that need special casing. - // Regular attributes that just accept strings. - case 'accentHeight': - setValueForAttribute(domElement, 'accent-height', value); - break; - case 'alignmentBaseline': - setValueForAttribute(domElement, 'alignment-baseline', value); - break; - case 'arabicForm': - setValueForAttribute(domElement, 'arabic-form', value); - break; - case 'baselineShift': - setValueForAttribute(domElement, 'baseline-shift', value); - break; - case 'capHeight': - setValueForAttribute(domElement, 'cap-height', value); - break; - case 'clipPath': - setValueForAttribute(domElement, 'clip-path', value); - break; - case 'clipRule': - setValueForAttribute(domElement, 'clip-rule', value); - break; - case 'colorInterpolation': - setValueForAttribute(domElement, 'color-interpolation', value); - break; - case 'colorInterpolationFilters': - setValueForAttribute(domElement, 'color-interpolation-filters', value); - break; - case 'colorProfile': - setValueForAttribute(domElement, 'color-profile', value); - break; - case 'colorRendering': - setValueForAttribute(domElement, 'color-rendering', value); - break; - case 'dominantBaseline': - setValueForAttribute(domElement, 'dominant-baseline', value); - break; - case 'enableBackground': - setValueForAttribute(domElement, 'enable-background', value); - break; - case 'fillOpacity': - setValueForAttribute(domElement, 'fill-opacity', value); - break; - case 'fillRule': - setValueForAttribute(domElement, 'fill-rule', value); - break; - case 'floodColor': - setValueForAttribute(domElement, 'flood-color', value); - break; - case 'floodOpacity': - setValueForAttribute(domElement, 'flood-opacity', value); - break; - case 'fontFamily': - setValueForAttribute(domElement, 'font-family', value); - break; - case 'fontSize': - setValueForAttribute(domElement, 'font-size', value); - break; - case 'fontSizeAdjust': - setValueForAttribute(domElement, 'font-size-adjust', value); - break; - case 'fontStretch': - setValueForAttribute(domElement, 'font-stretch', value); - break; - case 'fontStyle': - setValueForAttribute(domElement, 'font-style', value); - break; - case 'fontVariant': - setValueForAttribute(domElement, 'font-variant', value); - break; - case 'fontWeight': - setValueForAttribute(domElement, 'font-weight', value); - break; - case 'glyphName': - setValueForAttribute(domElement, 'glyph-name', value); - break; - case 'glyphOrientationHorizontal': - setValueForAttribute(domElement, 'glyph-orientation-horizontal', value); - break; - case 'glyphOrientationVertical': - setValueForAttribute(domElement, 'glyph-orientation-vertical', value); - break; - case 'horizAdvX': - setValueForAttribute(domElement, 'horiz-adv-x', value); - break; - case 'horizOriginX': - setValueForAttribute(domElement, 'horiz-origin-x', value); - break; - case 'imageRendering': - setValueForAttribute(domElement, 'image-rendering', value); - break; - case 'letterSpacing': - setValueForAttribute(domElement, 'letter-spacing', value); - break; - case 'lightingColor': - setValueForAttribute(domElement, 'lighting-color', value); - break; - case 'markerEnd': - setValueForAttribute(domElement, 'marker-end', value); - break; - case 'markerMid': - setValueForAttribute(domElement, 'marker-mid', value); - break; - case 'markerStart': - setValueForAttribute(domElement, 'marker-start', value); - break; - case 'overlinePosition': - setValueForAttribute(domElement, 'overline-position', value); - break; - case 'overlineThickness': - setValueForAttribute(domElement, 'overline-thickness', value); - break; - case 'paintOrder': - setValueForAttribute(domElement, 'paint-order', value); - break; - case 'panose-1': - setValueForAttribute(domElement, 'panose-1', value); - break; - case 'pointerEvents': - setValueForAttribute(domElement, 'pointer-events', value); - break; - case 'renderingIntent': - setValueForAttribute(domElement, 'rendering-intent', value); - break; - case 'shapeRendering': - setValueForAttribute(domElement, 'shape-rendering', value); - break; - case 'stopColor': - setValueForAttribute(domElement, 'stop-color', value); - break; - case 'stopOpacity': - setValueForAttribute(domElement, 'stop-opacity', value); - break; - case 'strikethroughPosition': - setValueForAttribute(domElement, 'strikethrough-position', value); - break; - case 'strikethroughThickness': - setValueForAttribute(domElement, 'strikethrough-thickness', value); - break; - case 'strokeDasharray': - setValueForAttribute(domElement, 'stroke-dasharray', value); - break; - case 'strokeDashoffset': - setValueForAttribute(domElement, 'stroke-dashoffset', value); - break; - case 'strokeLinecap': - setValueForAttribute(domElement, 'stroke-linecap', value); - break; - case 'strokeLinejoin': - setValueForAttribute(domElement, 'stroke-linejoin', value); - break; - case 'strokeMiterlimit': - setValueForAttribute(domElement, 'stroke-miterlimit', value); - break; - case 'strokeOpacity': - setValueForAttribute(domElement, 'stroke-opacity', value); - break; - case 'strokeWidth': - setValueForAttribute(domElement, 'stroke-width', value); - break; - case 'textAnchor': - setValueForAttribute(domElement, 'text-anchor', value); - break; - case 'textDecoration': - setValueForAttribute(domElement, 'text-decoration', value); - break; - case 'textRendering': - setValueForAttribute(domElement, 'text-rendering', value); - break; - case 'transformOrigin': - setValueForAttribute(domElement, 'transform-origin', value); - break; - case 'underlinePosition': - setValueForAttribute(domElement, 'underline-position', value); - break; - case 'underlineThickness': - setValueForAttribute(domElement, 'underline-thickness', value); - break; - case 'unicodeBidi': - setValueForAttribute(domElement, 'unicode-bidi', value); - break; - case 'unicodeRange': - setValueForAttribute(domElement, 'unicode-range', value); - break; - case 'unitsPerEm': - setValueForAttribute(domElement, 'units-per-em', value); - break; - case 'vAlphabetic': - setValueForAttribute(domElement, 'v-alphabetic', value); - break; - case 'vHanging': - setValueForAttribute(domElement, 'v-hanging', value); - break; - case 'vIdeographic': - setValueForAttribute(domElement, 'v-ideographic', value); - break; - case 'vMathematical': - setValueForAttribute(domElement, 'v-mathematical', value); - break; - case 'vectorEffect': - setValueForAttribute(domElement, 'vector-effect', value); - break; - case 'vertAdvY': - setValueForAttribute(domElement, 'vert-adv-y', value); - break; - case 'vertOriginX': - setValueForAttribute(domElement, 'vert-origin-x', value); - break; - case 'vertOriginY': - setValueForAttribute(domElement, 'vert-origin-y', value); - break; - case 'wordSpacing': - setValueForAttribute(domElement, 'word-spacing', value); - break; - case 'writingMode': - setValueForAttribute(domElement, 'writing-mode', value); - break; - case 'xmlnsXlink': - setValueForAttribute(domElement, 'xmlns:xlink', value); - break; - case 'xHeight': - setValueForAttribute(domElement, 'x-height', value); - break; case 'xlinkActuate': setValueForNamespacedAttribute( domElement, @@ -904,7 +679,8 @@ function setProp( warnForInvalidEventListener(key, value); } } else { - setValueForAttribute(domElement, key, value); + const attributeName = getAttributeAlias(key); + setValueForAttribute(domElement, attributeName, value); } } } @@ -1018,6 +794,17 @@ export function setInitialProperties( // TODO: Make sure that we check isMounted before firing any of these events. switch (tag) { + case 'div': + case 'span': + case 'svg': + case 'path': + case 'a': + case 'g': + case 'p': + case 'li': { + // Fast track the most common tag types + break; + } case 'input': { ReactDOMInputInitWrapperState(domElement, props); // We listen to this event in case to ensure emulated bubble @@ -1032,6 +819,21 @@ export function setInitialProperties( continue; } switch (propKey) { + case 'type': { + // Fast path since 'type' is very common on inputs + if ( + propValue != null && + typeof propValue !== 'function' && + typeof propValue !== 'symbol' && + typeof propValue !== 'boolean' + ) { + if (__DEV__) { + checkAttributeStringCoercion(propValue, propKey); + } + domElement.setAttribute(propKey, propValue); + } + break; + } case 'checked': { const node = ((domElement: any): InputWithWrapperState); const checked = @@ -1246,30 +1048,32 @@ export function setInitialProperties( } return; } + default: { + if (isCustomElement(tag, props)) { + for (const propKey in props) { + if (!props.hasOwnProperty(propKey)) { + continue; + } + const propValue = props[propKey]; + if (propValue == null) { + continue; + } + setPropOnCustomElement(domElement, tag, propKey, propValue, props); + } + return; + } + } } - if (isCustomElement(tag, props)) { - for (const propKey in props) { - if (!props.hasOwnProperty(propKey)) { - continue; - } - const propValue = props[propKey]; - if (propValue == null) { - continue; - } - setPropOnCustomElement(domElement, tag, propKey, propValue, props); + for (const propKey in props) { + if (!props.hasOwnProperty(propKey)) { + continue; } - } else { - for (const propKey in props) { - if (!props.hasOwnProperty(propKey)) { - continue; - } - const propValue = props[propKey]; - if (propValue == null) { - continue; - } - setProp(domElement, tag, propKey, propValue, props); + const propValue = props[propKey]; + if (propValue == null) { + continue; } + setProp(domElement, tag, propKey, propValue, props); } } @@ -1395,7 +1199,18 @@ export function updateProperties( nextProps: Object, ): void { switch (tag) { - case 'input': { + case 'div': + case 'span': + case 'svg': + case 'path': + case 'a': + case 'g': + case 'p': + case 'li': { + // Fast track the most common tag types + break; + } + case 'input': { // Update checked *before* name. // In the middle of an update, it is possible to have multiple checked. // When a checked radio tries to change name, browser makes another radio's checked false. @@ -1552,21 +1367,29 @@ export function updateProperties( } return; } + default: { + if (isCustomElement(tag, nextProps)) { + for (let i = 0; i < updatePayload.length; i += 2) { + const propKey = updatePayload[i]; + const propValue = updatePayload[i + 1]; + setPropOnCustomElement( + domElement, + tag, + propKey, + propValue, + nextProps, + ); + } + return; + } + } } // Apply the diff. - if (isCustomElement(tag, nextProps)) { - for (let i = 0; i < updatePayload.length; i += 2) { - const propKey = updatePayload[i]; - const propValue = updatePayload[i + 1]; - setPropOnCustomElement(domElement, tag, propKey, propValue, nextProps); - } - } else { - for (let i = 0; i < updatePayload.length; i += 2) { - const propKey = updatePayload[i]; - const propValue = updatePayload[i + 1]; - setProp(domElement, tag, propKey, propValue, nextProps); - } + for (let i = 0; i < updatePayload.length; i += 2) { + const propKey = updatePayload[i]; + const propValue = updatePayload[i + 1]; + setProp(domElement, tag, propKey, propValue, nextProps); } } @@ -2048,6 +1871,18 @@ function diffHydratedGenericElement( warnForPropDifference(propKey, serverHTML, expectedHTML); } continue; + case 'className': + hydrateAttribute(domElement, propKey, 'class', value, extraAttributes); + continue; + case 'tabIndex': + hydrateAttribute( + domElement, + propKey, + 'tabindex', + value, + extraAttributes, + ); + continue; case 'style': extraAttributes.delete(propKey); diffHydratedStyles(domElement, value); @@ -2244,828 +2079,132 @@ function diffHydratedGenericElement( ); continue; } - // A few React string attributes have a different name. - // This is a mapping from React prop names to the attribute names. - case 'acceptCharset': - hydrateAttribute( - domElement, - propKey, - 'accept-charset', - value, - extraAttributes, - ); - continue; - case 'className': - hydrateAttribute(domElement, propKey, 'class', value, extraAttributes); - continue; - case 'htmlFor': - hydrateAttribute(domElement, propKey, 'for', value, extraAttributes); - continue; - case 'httpEquiv': - hydrateAttribute( - domElement, - propKey, - 'http-equiv', - value, - extraAttributes, - ); - continue; - case 'tabIndex': - hydrateAttribute( - domElement, - propKey, - 'tabindex', - value, - extraAttributes, - ); - continue; - case 'crossOrigin': - hydrateAttribute( - domElement, - propKey, - 'crossorigin', - value, - extraAttributes, - ); - continue; - case 'accentHeight': - hydrateAttribute( - domElement, - propKey, - 'accent-height', - value, - extraAttributes, - ); - continue; - case 'alignmentBaseline': - hydrateAttribute( - domElement, - propKey, - 'alignment-baseline', - value, - extraAttributes, - ); - continue; - case 'arabicForm': - hydrateAttribute( - domElement, - propKey, - 'arabic-form', - value, - extraAttributes, - ); - continue; - case 'baselineShift': - hydrateAttribute( - domElement, - propKey, - 'baseline-shift', - value, - extraAttributes, - ); - continue; - case 'capHeight': - hydrateAttribute( - domElement, - propKey, - 'cap-height', - value, - extraAttributes, - ); - continue; - case 'clipPath': - hydrateAttribute( - domElement, - propKey, - 'clip-path', - value, - extraAttributes, - ); - continue; - case 'clipRule': - hydrateAttribute( - domElement, - propKey, - 'clip-rule', - value, - extraAttributes, - ); - continue; - case 'colorInterpolation': - hydrateAttribute( - domElement, - propKey, - 'color-interpolation', - value, - extraAttributes, - ); - continue; - case 'colorInterpolationFilters': - hydrateAttribute( - domElement, - propKey, - 'color-interpolation-filters', - value, - extraAttributes, - ); - continue; - case 'colorProfile': - hydrateAttribute( - domElement, - propKey, - 'color-profile', - value, - extraAttributes, - ); - continue; - case 'colorRendering': - hydrateAttribute( - domElement, - propKey, - 'color-rendering', - value, - extraAttributes, - ); - continue; - case 'dominantBaseline': - hydrateAttribute( - domElement, - propKey, - 'dominant-baseline', - value, - extraAttributes, - ); - continue; - case 'enableBackground': - hydrateAttribute( - domElement, - propKey, - 'enable-background', - value, - extraAttributes, - ); - continue; - case 'fillOpacity': - hydrateAttribute( - domElement, - propKey, - 'fill-opacity', - value, - extraAttributes, - ); - continue; - case 'fillRule': - hydrateAttribute( - domElement, - propKey, - 'fill-rule', - value, - extraAttributes, - ); - continue; - case 'floodColor': - hydrateAttribute( - domElement, - propKey, - 'flood-color', - value, - extraAttributes, - ); - continue; - case 'floodOpacity': - hydrateAttribute( - domElement, - propKey, - 'flood-opacity', - value, - extraAttributes, - ); - continue; - case 'fontFamily': - hydrateAttribute( - domElement, - propKey, - 'font-family', - value, - extraAttributes, - ); - continue; - case 'fontSize': - hydrateAttribute( - domElement, - propKey, - 'font-size', - value, - extraAttributes, - ); - continue; - case 'fontSizeAdjust': - hydrateAttribute( - domElement, - propKey, - 'font-size-adjust', - value, - extraAttributes, - ); - continue; - case 'fontStretch': + case 'xHeight': hydrateAttribute( domElement, propKey, - 'font-stretch', + 'x-height', value, extraAttributes, ); continue; - case 'fontStyle': + case 'xlinkActuate': hydrateAttribute( domElement, propKey, - 'font-style', + 'xlink:actuate', value, extraAttributes, ); continue; - case 'fontVariant': + case 'xlinkArcrole': hydrateAttribute( domElement, propKey, - 'font-variant', + 'xlink:arcrole', value, extraAttributes, ); continue; - case 'fontWeight': + case 'xlinkRole': hydrateAttribute( domElement, propKey, - 'font-weight', + 'xlink:role', value, extraAttributes, ); continue; - case 'glyphName': + case 'xlinkShow': hydrateAttribute( domElement, propKey, - 'glyph-name', + 'xlink:show', value, extraAttributes, ); continue; - case 'glyphOrientationHorizontal': + case 'xlinkTitle': hydrateAttribute( domElement, propKey, - 'glyph-orientation-horizontal', + 'xlink:title', value, extraAttributes, ); continue; - case 'glyphOrientationVertical': + case 'xlinkType': hydrateAttribute( domElement, propKey, - 'glyph-orientation-vertical', + 'xlink:type', value, extraAttributes, ); continue; - case 'horizAdvX': + case 'xmlBase': hydrateAttribute( domElement, propKey, - 'horiz-adv-x', + 'xml:base', value, extraAttributes, ); continue; - case 'horizOriginX': + case 'xmlLang': hydrateAttribute( domElement, propKey, - 'horiz-origin-x', + 'xml:lang', value, extraAttributes, ); continue; - case 'imageRendering': + case 'xmlSpace': hydrateAttribute( domElement, propKey, - 'image-rendering', + 'xml:space', value, extraAttributes, ); continue; - case 'letterSpacing': - hydrateAttribute( + default: { + if ( + // shouldIgnoreAttribute + // We have already filtered out null/undefined and reserved words. + propKey.length > 2 && + (propKey[0] === 'o' || propKey[0] === 'O') && + (propKey[1] === 'n' || propKey[1] === 'N') + ) { + continue; + } + const attributeName = getAttributeAlias(propKey); + let isMismatchDueToBadCasing = false; + let ownNamespaceDev = parentNamespaceDev; + if (ownNamespaceDev === HTML_NAMESPACE) { + ownNamespaceDev = getIntrinsicNamespace(tag); + } + if (ownNamespaceDev === HTML_NAMESPACE) { + extraAttributes.delete(attributeName.toLowerCase()); + } else { + const standardName = getPossibleStandardName(propKey); + if (standardName !== null && standardName !== propKey) { + // If an SVG prop is supplied with bad casing, it will + // be successfully parsed from HTML, but will produce a mismatch + // (and would be incorrectly rendered on the client). + // However, we already warn about bad casing elsewhere. + // So we'll skip the misleading extra mismatch warning in this case. + isMismatchDueToBadCasing = true; + extraAttributes.delete(standardName); + } + extraAttributes.delete(attributeName); + } + const serverValue = getValueForAttribute( domElement, - propKey, - 'letter-spacing', + attributeName, value, - extraAttributes, ); - continue; - case 'lightingColor': - hydrateAttribute( - domElement, - propKey, - 'lighting-color', - value, - extraAttributes, - ); - continue; - case 'markerEnd': - hydrateAttribute( - domElement, - propKey, - 'marker-end', - value, - extraAttributes, - ); - continue; - case 'markerMid': - hydrateAttribute( - domElement, - propKey, - 'marker-mid', - value, - extraAttributes, - ); - continue; - case 'markerStart': - hydrateAttribute( - domElement, - propKey, - 'marker-start', - value, - extraAttributes, - ); - continue; - case 'overlinePosition': - hydrateAttribute( - domElement, - propKey, - 'overline-position', - value, - extraAttributes, - ); - continue; - case 'overlineThickness': - hydrateAttribute( - domElement, - propKey, - 'overline-thickness', - value, - extraAttributes, - ); - continue; - case 'paintOrder': - hydrateAttribute( - domElement, - propKey, - 'paint-order', - value, - extraAttributes, - ); - continue; - case 'panose-1': - hydrateAttribute( - domElement, - propKey, - 'panose-1', - value, - extraAttributes, - ); - continue; - case 'pointerEvents': - hydrateAttribute( - domElement, - propKey, - 'pointer-events', - value, - extraAttributes, - ); - continue; - case 'renderingIntent': - hydrateAttribute( - domElement, - propKey, - 'rendering-intent', - value, - extraAttributes, - ); - continue; - case 'shapeRendering': - hydrateAttribute( - domElement, - propKey, - 'shape-rendering', - value, - extraAttributes, - ); - continue; - case 'stopColor': - hydrateAttribute( - domElement, - propKey, - 'stop-color', - value, - extraAttributes, - ); - continue; - case 'stopOpacity': - hydrateAttribute( - domElement, - propKey, - 'stop-opacity', - value, - extraAttributes, - ); - continue; - case 'strikethroughPosition': - hydrateAttribute( - domElement, - propKey, - 'strikethrough-position', - value, - extraAttributes, - ); - continue; - case 'strikethroughThickness': - hydrateAttribute( - domElement, - propKey, - 'strikethrough-thickness', - value, - extraAttributes, - ); - continue; - case 'strokeDasharray': - hydrateAttribute( - domElement, - propKey, - 'stroke-dasharray', - value, - extraAttributes, - ); - continue; - case 'strokeDashoffset': - hydrateAttribute( - domElement, - propKey, - 'stroke-dashoffset', - value, - extraAttributes, - ); - continue; - case 'strokeLinecap': - hydrateAttribute( - domElement, - propKey, - 'stroke-linecap', - value, - extraAttributes, - ); - continue; - case 'strokeLinejoin': - hydrateAttribute( - domElement, - propKey, - 'stroke-linejoin', - value, - extraAttributes, - ); - continue; - case 'strokeMiterlimit': - hydrateAttribute( - domElement, - propKey, - 'stroke-miterlimit', - value, - extraAttributes, - ); - continue; - case 'strokeOpacity': - hydrateAttribute( - domElement, - propKey, - 'stroke-opacity', - value, - extraAttributes, - ); - continue; - case 'strokeWidth': - hydrateAttribute( - domElement, - propKey, - 'stroke-width', - value, - extraAttributes, - ); - continue; - case 'textAnchor': - hydrateAttribute( - domElement, - propKey, - 'text-anchor', - value, - extraAttributes, - ); - continue; - case 'textDecoration': - hydrateAttribute( - domElement, - propKey, - 'text-decoration', - value, - extraAttributes, - ); - continue; - case 'textRendering': - hydrateAttribute( - domElement, - propKey, - 'text-rendering', - value, - extraAttributes, - ); - continue; - case 'transformOrigin': - hydrateAttribute( - domElement, - propKey, - 'transform-origin', - value, - extraAttributes, - ); - continue; - case 'underlinePosition': - hydrateAttribute( - domElement, - propKey, - 'underline-position', - value, - extraAttributes, - ); - continue; - case 'underlineThickness': - hydrateAttribute( - domElement, - propKey, - 'underline-thickness', - value, - extraAttributes, - ); - continue; - case 'unicodeBidi': - hydrateAttribute( - domElement, - propKey, - 'unicode-bidi', - value, - extraAttributes, - ); - continue; - case 'unicodeRange': - hydrateAttribute( - domElement, - propKey, - 'unicode-range', - value, - extraAttributes, - ); - continue; - case 'unitsPerEm': - hydrateAttribute( - domElement, - propKey, - 'units-per-em', - value, - extraAttributes, - ); - continue; - case 'vAlphabetic': - hydrateAttribute( - domElement, - propKey, - 'v-alphabetic', - value, - extraAttributes, - ); - continue; - case 'vHanging': - hydrateAttribute( - domElement, - propKey, - 'v-hanging', - value, - extraAttributes, - ); - continue; - case 'vIdeographic': - hydrateAttribute( - domElement, - propKey, - 'v-ideographic', - value, - extraAttributes, - ); - continue; - case 'vMathematical': - hydrateAttribute( - domElement, - propKey, - 'v-mathematical', - value, - extraAttributes, - ); - continue; - case 'vectorEffect': - hydrateAttribute( - domElement, - propKey, - 'vector-effect', - value, - extraAttributes, - ); - continue; - case 'vertAdvY': - hydrateAttribute( - domElement, - propKey, - 'vert-adv-y', - value, - extraAttributes, - ); - continue; - case 'vertOriginX': - hydrateAttribute( - domElement, - propKey, - 'vert-origin-x', - value, - extraAttributes, - ); - continue; - case 'vertOriginY': - hydrateAttribute( - domElement, - propKey, - 'vert-origin-y', - value, - extraAttributes, - ); - continue; - case 'wordSpacing': - hydrateAttribute( - domElement, - propKey, - 'word-spacing', - value, - extraAttributes, - ); - continue; - case 'writingMode': - hydrateAttribute( - domElement, - propKey, - 'writing-mode', - value, - extraAttributes, - ); - continue; - case 'xmlnsXlink': - hydrateAttribute( - domElement, - propKey, - 'xmlns:xlink', - value, - extraAttributes, - ); - continue; - case 'xHeight': - hydrateAttribute( - domElement, - propKey, - 'x-height', - value, - extraAttributes, - ); - continue; - case 'xlinkActuate': - hydrateAttribute( - domElement, - propKey, - 'xlink:actuate', - value, - extraAttributes, - ); - continue; - case 'xlinkArcrole': - hydrateAttribute( - domElement, - propKey, - 'xlink:arcrole', - value, - extraAttributes, - ); - continue; - case 'xlinkRole': - hydrateAttribute( - domElement, - propKey, - 'xlink:role', - value, - extraAttributes, - ); - continue; - case 'xlinkShow': - hydrateAttribute( - domElement, - propKey, - 'xlink:show', - value, - extraAttributes, - ); - continue; - case 'xlinkTitle': - hydrateAttribute( - domElement, - propKey, - 'xlink:title', - value, - extraAttributes, - ); - continue; - case 'xlinkType': - hydrateAttribute( - domElement, - propKey, - 'xlink:type', - value, - extraAttributes, - ); - continue; - case 'xmlBase': - hydrateAttribute( - domElement, - propKey, - 'xml:base', - value, - extraAttributes, - ); - continue; - case 'xmlLang': - hydrateAttribute( - domElement, - propKey, - 'xml:lang', - value, - extraAttributes, - ); - continue; - case 'xmlSpace': - hydrateAttribute( - domElement, - propKey, - 'xml:space', - value, - extraAttributes, - ); - continue; - default: { - if ( - // shouldIgnoreAttribute - // We have already filtered out null/undefined and reserved words. - propKey.length > 2 && - (propKey[0] === 'o' || propKey[0] === 'O') && - (propKey[1] === 'n' || propKey[1] === 'N') - ) { - continue; - } - let isMismatchDueToBadCasing = false; - let ownNamespaceDev = parentNamespaceDev; - if (ownNamespaceDev === HTML_NAMESPACE) { - ownNamespaceDev = getIntrinsicNamespace(tag); - } - if (ownNamespaceDev === HTML_NAMESPACE) { - extraAttributes.delete(propKey.toLowerCase()); - } else { - const standardName = getPossibleStandardName(propKey); - if (standardName !== null && standardName !== propKey) { - // If an SVG prop is supplied with bad casing, it will - // be successfully parsed from HTML, but will produce a mismatch - // (and would be incorrectly rendered on the client). - // However, we already warn about bad casing elsewhere. - // So we'll skip the misleading extra mismatch warning in this case. - isMismatchDueToBadCasing = true; - extraAttributes.delete(standardName); - } - extraAttributes.delete(propKey); - } - const serverValue = getValueForAttribute(domElement, propKey, value); if (!isMismatchDueToBadCasing) { warnForPropDifference(propKey, serverValue, value); } diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index 601706df5015..2df5fb332fa1 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -40,6 +40,7 @@ import { import isAttributeNameSafe from '../shared/isAttributeNameSafe'; import isUnitlessNumber from '../shared/isUnitlessNumber'; +import getAttributeAlias from '../shared/getAttributeAlias'; import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; @@ -640,23 +641,29 @@ function pushAttribute( value: string | boolean | number | Function | Object, // not null or undefined ): void { switch (name) { + // These are very common props and therefore are in the beginning of the switch. + // TODO: aria-label is a very common prop but allows booleans so is not like the others + // but should ideally go in this list too. + case 'className': { + pushStringAttribute(target, 'class', value); + break; + } + case 'tabIndex': { + pushStringAttribute(target, 'tabindex', value); + break; + } + case 'dir': + case 'role': + case 'viewBox': + case 'width': + case 'height': { + pushStringAttribute(target, name, value); + break; + } case 'style': { pushStyleAttribute(target, value); return; } - case 'defaultValue': - case 'defaultChecked': // These shouldn't be set as attributes on generic HTML elements. - case 'innerHTML': // Must use dangerouslySetInnerHTML instead. - case 'suppressContentEditableWarning': - case 'suppressHydrationWarning': - // Ignored. These are built-in to React on the client. - return; - case 'autoFocus': - case 'multiple': - case 'muted': { - pushBooleanAttribute(target, name.toLowerCase(), value); - return; - } case 'src': case 'href': case 'action': @@ -709,6 +716,19 @@ function pushAttribute( ); return; } + case 'defaultValue': + case 'defaultChecked': // These shouldn't be set as attributes on generic HTML elements. + case 'innerHTML': // Must use dangerouslySetInnerHTML instead. + case 'suppressContentEditableWarning': + case 'suppressHydrationWarning': + // Ignored. These are built-in to React on the client. + return; + case 'autoFocus': + case 'multiple': + case 'muted': { + pushBooleanAttribute(target, name.toLowerCase(), value); + return; + } case 'xlinkHref': { if ( typeof value === 'function' || @@ -846,278 +866,33 @@ function pushAttribute( } return; } - // A few React string attributes have a different name. - // This is a mapping from React prop names to the attribute names. - case 'acceptCharset': - pushStringAttribute(target, 'accept-charset', value); - return; - case 'className': - pushStringAttribute(target, 'class', value); - return; - case 'htmlFor': - pushStringAttribute(target, 'for', value); - return; - case 'httpEquiv': - pushStringAttribute(target, 'http-equiv', value); - return; - // HTML and SVG attributes, but the SVG attribute is case sensitive. - case 'tabIndex': - pushStringAttribute(target, 'tabindex', value); - return; - case 'crossOrigin': - pushStringAttribute(target, 'crossorigin', value); - return; - // This is a list of all SVG attributes that need special casing. - // Regular attributes that just accept strings. - case 'accentHeight': - pushStringAttribute(target, 'accent-height', value); - return; - case 'alignmentBaseline': - pushStringAttribute(target, 'alignment-baseline', value); - return; - case 'arabicForm': - pushStringAttribute(target, 'arabic-form', value); - return; - case 'baselineShift': - pushStringAttribute(target, 'baseline-shift', value); - return; - case 'capHeight': - pushStringAttribute(target, 'cap-height', value); - return; - case 'clipPath': - pushStringAttribute(target, 'clip-path', value); - return; - case 'clipRule': - pushStringAttribute(target, 'clip-rule', value); - return; - case 'colorInterpolation': - pushStringAttribute(target, 'color-interpolation', value); - return; - case 'colorInterpolationFilters': - pushStringAttribute(target, 'color-interpolation-filters', value); - return; - case 'colorProfile': - pushStringAttribute(target, 'color-profile', value); - return; - case 'colorRendering': - pushStringAttribute(target, 'color-rendering', value); - return; - case 'dominantBaseline': - pushStringAttribute(target, 'dominant-baseline', value); - return; - case 'enableBackground': - pushStringAttribute(target, 'enable-background', value); - return; - case 'fillOpacity': - pushStringAttribute(target, 'fill-opacity', value); - return; - case 'fillRule': - pushStringAttribute(target, 'fill-rule', value); - return; - case 'floodColor': - pushStringAttribute(target, 'flood-color', value); - return; - case 'floodOpacity': - pushStringAttribute(target, 'flood-opacity', value); - return; - case 'fontFamily': - pushStringAttribute(target, 'font-family', value); - return; - case 'fontSize': - pushStringAttribute(target, 'font-size', value); - return; - case 'fontSizeAdjust': - pushStringAttribute(target, 'font-size-adjust', value); - return; - case 'fontStretch': - pushStringAttribute(target, 'font-stretch', value); - return; - case 'fontStyle': - pushStringAttribute(target, 'font-style', value); - return; - case 'fontVariant': - pushStringAttribute(target, 'font-variant', value); - return; - case 'fontWeight': - pushStringAttribute(target, 'font-weight', value); - return; - case 'glyphName': - pushStringAttribute(target, 'glyph-name', value); - return; - case 'glyphOrientationHorizontal': - pushStringAttribute(target, 'glyph-orientation-horizontal', value); - return; - case 'glyphOrientationVertical': - pushStringAttribute(target, 'glyph-orientation-vertical', value); - return; - case 'horizAdvX': - pushStringAttribute(target, 'horiz-adv-x', value); - return; - case 'horizOriginX': - pushStringAttribute(target, 'horiz-origin-x', value); - return; - case 'imageRendering': - pushStringAttribute(target, 'image-rendering', value); - return; - case 'letterSpacing': - pushStringAttribute(target, 'letter-spacing', value); - return; - case 'lightingColor': - pushStringAttribute(target, 'lighting-color', value); - return; - case 'markerEnd': - pushStringAttribute(target, 'marker-end', value); - return; - case 'markerMid': - pushStringAttribute(target, 'marker-mid', value); - return; - case 'markerStart': - pushStringAttribute(target, 'marker-start', value); - return; - case 'overlinePosition': - pushStringAttribute(target, 'overline-position', value); - return; - case 'overlineThickness': - pushStringAttribute(target, 'overline-thickness', value); - return; - case 'paintOrder': - pushStringAttribute(target, 'paint-order', value); - return; - case 'panose-1': - pushStringAttribute(target, 'panose-1', value); - return; - case 'pointerEvents': - pushStringAttribute(target, 'pointer-events', value); - return; - case 'renderingIntent': - pushStringAttribute(target, 'rendering-intent', value); - return; - case 'shapeRendering': - pushStringAttribute(target, 'shape-rendering', value); - return; - case 'stopColor': - pushStringAttribute(target, 'stop-color', value); - return; - case 'stopOpacity': - pushStringAttribute(target, 'stop-opacity', value); - return; - case 'strikethroughPosition': - pushStringAttribute(target, 'strikethrough-position', value); - return; - case 'strikethroughThickness': - pushStringAttribute(target, 'strikethrough-thickness', value); - return; - case 'strokeDasharray': - pushStringAttribute(target, 'stroke-dasharray', value); - return; - case 'strokeDashoffset': - pushStringAttribute(target, 'stroke-dashoffset', value); - return; - case 'strokeLinecap': - pushStringAttribute(target, 'stroke-linecap', value); - return; - case 'strokeLinejoin': - pushStringAttribute(target, 'stroke-linejoin', value); - return; - case 'strokeMiterlimit': - pushStringAttribute(target, 'stroke-miterlimit', value); - return; - case 'strokeOpacity': - pushStringAttribute(target, 'stroke-opacity', value); - return; - case 'strokeWidth': - pushStringAttribute(target, 'stroke-width', value); - return; - case 'textAnchor': - pushStringAttribute(target, 'text-anchor', value); - return; - case 'textDecoration': - pushStringAttribute(target, 'text-decoration', value); - return; - case 'textRendering': - pushStringAttribute(target, 'text-rendering', value); - return; - case 'transformOrigin': - pushStringAttribute(target, 'transform-origin', value); - return; - case 'underlinePosition': - pushStringAttribute(target, 'underline-position', value); - return; - case 'underlineThickness': - pushStringAttribute(target, 'underline-thickness', value); - return; - case 'unicodeBidi': - pushStringAttribute(target, 'unicode-bidi', value); - return; - case 'unicodeRange': - pushStringAttribute(target, 'unicode-range', value); - return; - case 'unitsPerEm': - pushStringAttribute(target, 'units-per-em', value); - return; - case 'vAlphabetic': - pushStringAttribute(target, 'v-alphabetic', value); - return; - case 'vHanging': - pushStringAttribute(target, 'v-hanging', value); - return; - case 'vIdeographic': - pushStringAttribute(target, 'v-ideographic', value); - return; - case 'vMathematical': - pushStringAttribute(target, 'v-mathematical', value); - return; - case 'vectorEffect': - pushStringAttribute(target, 'vector-effect', value); - return; - case 'vertAdvY': - pushStringAttribute(target, 'vert-adv-y', value); - return; - case 'vertOriginX': - pushStringAttribute(target, 'vert-origin-x', value); - return; - case 'vertOriginY': - pushStringAttribute(target, 'vert-origin-y', value); - return; - case 'wordSpacing': - pushStringAttribute(target, 'word-spacing', value); - return; - case 'writingMode': - pushStringAttribute(target, 'writing-mode', value); - return; - case 'xmlnsXlink': - pushStringAttribute(target, 'xmlns:xlink', value); - return; - case 'xHeight': - pushStringAttribute(target, 'x-height', value); - return; case 'xlinkActuate': pushStringAttribute(target, 'xlink:actuate', value); - break; + return; case 'xlinkArcrole': pushStringAttribute(target, 'xlink:arcrole', value); - break; + return; case 'xlinkRole': pushStringAttribute(target, 'xlink:role', value); - break; + return; case 'xlinkShow': pushStringAttribute(target, 'xlink:show', value); - break; + return; case 'xlinkTitle': pushStringAttribute(target, 'xlink:title', value); - break; + return; case 'xlinkType': pushStringAttribute(target, 'xlink:type', value); - break; + return; case 'xmlBase': pushStringAttribute(target, 'xml:base', value); - break; + return; case 'xmlLang': pushStringAttribute(target, 'xml:lang', value); - break; + return; case 'xmlSpace': pushStringAttribute(target, 'xml:space', value); - break; + return; default: if ( // shouldIgnoreAttribute @@ -1129,14 +904,15 @@ function pushAttribute( return; } - if (isAttributeNameSafe(name)) { + const attributeName = getAttributeAlias(name); + if (isAttributeNameSafe(attributeName)) { // shouldRemoveAttribute switch (typeof value) { case 'function': case 'symbol': // eslint-disable-line return; case 'boolean': { - const prefix = name.toLowerCase().slice(0, 5); + const prefix = attributeName.toLowerCase().slice(0, 5); if (prefix !== 'data-' && prefix !== 'aria-') { return; } @@ -1144,7 +920,7 @@ function pushAttribute( } target.push( attributeSeparator, - stringToChunk(name), + stringToChunk(attributeName), attributeAssign, stringToChunk(escapeTextForBrowser(value)), attributeEnd, @@ -2862,6 +2638,16 @@ export function pushStartInstance( } switch (type) { + case 'div': + case 'span': + case 'svg': + case 'path': + case 'a': + case 'g': + case 'p': + case 'li': + // Fast track very common tags + break; // Special tags case 'select': return pushStartSelect(target, props); @@ -2971,15 +2757,14 @@ export function pushStartInstance( ); } default: { - if (type.indexOf('-') === -1) { - // Generic element - return pushStartGenericElement(target, props, type); - } else { + if (type.indexOf('-') !== -1) { // Custom element return pushStartCustomElement(target, props, type); } } } + // Generic element + return pushStartGenericElement(target, props, type); } const endTag1 = stringToPrecomputedChunk('