react/packages/react-dom/src/client/ReactDOMComponent.js
Rick Hanlon 655affa302 Clarifications
Co-authored-by: shengxinjing <316783812@qq.com>
2020-06-12 21:09:29 -04:00

1400 lines
48 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its 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 {registrationNameModules} from 'legacy-events/EventPluginRegistry';
import {canUseDOM} from 'shared/ExecutionEnvironment';
import invariant from 'shared/invariant';
import {
setListenToResponderEventTypes,
addResponderEventSystemEvent,
removeTrappedEventListener,
} from '../events/DeprecatedDOMEventResponderSystem';
import {
getValueForAttribute,
getValueForProperty,
setValueForProperty,
} from './DOMPropertyOperations';
import {
initWrapperState as ReactDOMInputInitWrapperState,
getHostProps as ReactDOMInputGetHostProps,
postMountWrapper as ReactDOMInputPostMountWrapper,
updateChecked as ReactDOMInputUpdateChecked,
updateWrapper as ReactDOMInputUpdateWrapper,
restoreControlledState as ReactDOMInputRestoreControlledState,
} from './ReactDOMInput';
import {
getHostProps as ReactDOMOptionGetHostProps,
postMountWrapper as ReactDOMOptionPostMountWrapper,
validateProps as ReactDOMOptionValidateProps,
} from './ReactDOMOption';
import {
initWrapperState as ReactDOMSelectInitWrapperState,
getHostProps as ReactDOMSelectGetHostProps,
postMountWrapper as ReactDOMSelectPostMountWrapper,
restoreControlledState as ReactDOMSelectRestoreControlledState,
postUpdateWrapper as ReactDOMSelectPostUpdateWrapper,
} from './ReactDOMSelect';
import {
initWrapperState as ReactDOMTextareaInitWrapperState,
getHostProps as ReactDOMTextareaGetHostProps,
postMountWrapper as ReactDOMTextareaPostMountWrapper,
updateWrapper as ReactDOMTextareaUpdateWrapper,
restoreControlledState as ReactDOMTextareaRestoreControlledState,
} from './ReactDOMTextarea';
import {track} from './inputValueTracking';
import setInnerHTML from './setInnerHTML';
import setTextContent from './setTextContent';
import {
TOP_ERROR,
TOP_INVALID,
TOP_LOAD,
TOP_RESET,
TOP_SUBMIT,
TOP_TOGGLE,
} from '../events/DOMTopLevelEventTypes';
import {mediaEventTypes} from '../events/DOMTopLevelEventTypes';
import {
createDangerousStringForStyles,
setValueForStyles,
validateShorthandPropertyCollisionInDev,
} from '../shared/CSSPropertyOperations';
import {Namespaces, getIntrinsicNamespace} from '../shared/DOMNamespaces';
import {
getPropertyInfo,
shouldIgnoreAttribute,
shouldRemoveAttribute,
} from '../shared/DOMProperty';
import assertValidProps from '../shared/assertValidProps';
import {
DOCUMENT_NODE,
DOCUMENT_FRAGMENT_NODE,
ELEMENT_NODE,
COMMENT_NODE,
} from '../shared/HTMLNodeType';
import isCustomComponent from '../shared/isCustomComponent';
import possibleStandardNames from '../shared/possibleStandardNames';
import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols';
import {
enableDeprecatedFlareAPI,
enableTrustedTypesIntegration,
enableModernEventSystem,
} from 'shared/ReactFeatureFlags';
import {
legacyListenToEvent,
legacyTrapBubbledEvent,
} from '../events/DOMLegacyEventPluginSystem';
import {listenToEvent} from '../events/DOMModernPluginEventSystem';
import {getEventListenerMap} from './ReactDOMComponentTree';
let didWarnInvalidHydration = false;
let didWarnScriptTags = false;
const DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML';
const SUPPRESS_CONTENT_EDITABLE_WARNING = 'suppressContentEditableWarning';
const SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
const AUTOFOCUS = 'autoFocus';
const CHILDREN = 'children';
const STYLE = 'style';
const HTML = '__html';
const DEPRECATED_flareListeners = 'DEPRECATED_flareListeners';
const {html: HTML_NAMESPACE} = Namespaces;
let warnedUnknownTags;
let suppressHydrationWarning;
let validatePropertiesInDevelopment;
let warnForTextDifference;
let warnForPropDifference;
let warnForExtraAttributes;
let warnForInvalidEventListener;
let canDiffStyleForHydrationWarning;
let normalizeMarkupForTextOrAttribute;
let normalizeHTML;
if (__DEV__) {
warnedUnknownTags = {
// Chrome is the only major browser not shipping <time>. But as of July
// 2017 it intends to ship it due to widespread usage. We intentionally
// *don't* warn for <time> even if it's unrecognized by Chrome because
// it soon will be, and many apps have been using it anyway.
time: true,
// There are working polyfills for <dialog>. Let people use it.
dialog: true,
// Electron ships a custom <webview> tag to display external web content in
// an isolated frame and process.
// This tag is not present in non Electron environments such as JSDom which
// is often used for testing purposes.
// @see https://electronjs.org/docs/api/webview-tag
webview: true,
};
validatePropertiesInDevelopment = function(type, props) {
validateARIAProperties(type, props);
validateInputProperties(type, props);
validateUnknownProperties(type, props, /* canUseEventSystem */ true);
};
// IE 11 parses & normalizes the style attribute as opposed to other
// browsers. It adds spaces and sorts the properties in some
// non-alphabetical order. Handling that would require sorting CSS
// properties in the client & server versions or applying
// `expectedStyle` to a temporary DOM node to read its `style` attribute
// normalized. Since it only affects IE, we're skipping style warnings
// in that browser completely in favor of doing all that work.
// See https://github.com/facebook/react/issues/11807
canDiffStyleForHydrationWarning = canUseDOM && !document.documentMode;
// HTML parsing normalizes CR and CRLF to LF.
// It also can turn \u0000 into \uFFFD inside attributes.
// https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
// If we have a mismatch, it might be caused by that.
// We will still patch up in this case but not fire the warning.
const NORMALIZE_NEWLINES_REGEX = /\r\n?/g;
const NORMALIZE_NULL_AND_REPLACEMENT_REGEX = /\u0000|\uFFFD/g;
normalizeMarkupForTextOrAttribute = function(markup: mixed): string {
const markupString =
typeof markup === 'string' ? markup : '' + (markup: any);
return markupString
.replace(NORMALIZE_NEWLINES_REGEX, '\n')
.replace(NORMALIZE_NULL_AND_REPLACEMENT_REGEX, '');
};
warnForTextDifference = function(
serverText: string,
clientText: string | number,
) {
if (didWarnInvalidHydration) {
return;
}
const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText);
const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText);
if (normalizedServerText === normalizedClientText) {
return;
}
didWarnInvalidHydration = true;
console.error(
'Text content did not match. Server: "%s" Client: "%s"',
normalizedServerText,
normalizedClientText,
);
};
warnForPropDifference = function(
propName: string,
serverValue: mixed,
clientValue: mixed,
) {
if (didWarnInvalidHydration) {
return;
}
const normalizedClientValue = normalizeMarkupForTextOrAttribute(
clientValue,
);
const normalizedServerValue = normalizeMarkupForTextOrAttribute(
serverValue,
);
if (normalizedServerValue === normalizedClientValue) {
return;
}
didWarnInvalidHydration = true;
console.error(
'Prop `%s` did not match. Server: %s Client: %s',
propName,
JSON.stringify(normalizedServerValue),
JSON.stringify(normalizedClientValue),
);
};
warnForExtraAttributes = function(attributeNames: Set<string>) {
if (didWarnInvalidHydration) {
return;
}
didWarnInvalidHydration = true;
const names = [];
attributeNames.forEach(function(name) {
names.push(name);
});
console.error('Extra attributes from the server: %s', names);
};
warnForInvalidEventListener = function(registrationName, listener) {
if (listener === false) {
console.error(
'Expected `%s` listener to be a function, instead got `false`.\n\n' +
'If you used to conditionally omit it with %s={condition && value}, ' +
'pass %s={condition ? value : undefined} instead.',
registrationName,
registrationName,
registrationName,
);
} else {
console.error(
'Expected `%s` listener to be a function, instead got a value of `%s` type.',
registrationName,
typeof listener,
);
}
};
// Parse the HTML and read it back to normalize the HTML string so that it
// can be used for comparison.
normalizeHTML = function(parent: Element, html: string) {
// We could have created a separate document here to avoid
// re-initializing custom elements if they exist. But this breaks
// how <noscript> is being handled. So we use the same document.
// See the discussion in https://github.com/facebook/react/pull/11157.
const testElement =
parent.namespaceURI === HTML_NAMESPACE
? parent.ownerDocument.createElement(parent.tagName)
: parent.ownerDocument.createElementNS(
(parent.namespaceURI: any),
parent.tagName,
);
testElement.innerHTML = html;
return testElement.innerHTML;
};
}
export function ensureListeningTo(
rootContainerInstance: Element | Node,
registrationName: string,
): void {
if (enableModernEventSystem) {
// If we have a comment node, then use the parent node,
// which should be an element.
const rootContainerElement =
rootContainerInstance.nodeType === COMMENT_NODE
? rootContainerInstance.parentNode
: rootContainerInstance;
// Containers should only ever be element nodes. We do not
// want to register events to document fragments or documents
// with the modern plugin event system.
invariant(
rootContainerElement != null &&
rootContainerElement.nodeType === ELEMENT_NODE,
'ensureListeningTo(): received a container that was not an element node. ' +
'This is likely a bug in React.',
);
listenToEvent(registrationName, ((rootContainerElement: any): Element));
} else {
// Legacy plugin event system path
const isDocumentOrFragment =
rootContainerInstance.nodeType === DOCUMENT_NODE ||
rootContainerInstance.nodeType === DOCUMENT_FRAGMENT_NODE;
const doc = isDocumentOrFragment
? rootContainerInstance
: rootContainerInstance.ownerDocument;
legacyListenToEvent(registrationName, ((doc: any): Document));
}
}
function getOwnerDocumentFromRootContainer(
rootContainerElement: Element | Document,
): Document {
return rootContainerElement.nodeType === DOCUMENT_NODE
? (rootContainerElement: any)
: rootContainerElement.ownerDocument;
}
function noop() {}
export function trapClickOnNonInteractiveElement(node: HTMLElement) {
// Mobile Safari does not fire properly bubble click events on
// non-interactive elements, which means delegated click listeners do not
// fire. The workaround for this bug involves attaching an empty click
// listener on the target node.
// http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
// Just set it using the onclick property so that we don't have to manage any
// bookkeeping for it. Not sure if we need to clear it when the listener is
// removed.
// TODO: Only do this for the relevant Safaris maybe?
node.onclick = noop;
}
function setInitialDOMProperties(
tag: string,
domElement: Element,
rootContainerElement: Element | Document,
nextProps: Object,
isCustomComponentTag: boolean,
): void {
for (const propKey in nextProps) {
if (!nextProps.hasOwnProperty(propKey)) {
continue;
}
const nextProp = nextProps[propKey];
if (propKey === STYLE) {
if (__DEV__) {
if (nextProp) {
// Freeze the next style object so that we can assume it won't be
// mutated. We have already warned for this in the past.
Object.freeze(nextProp);
}
}
// Relies on `updateStylesByID` not mutating `styleUpdates`.
setValueForStyles(domElement, nextProp);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
const nextHtml = nextProp ? nextProp[HTML] : undefined;
if (nextHtml != null) {
setInnerHTML(domElement, nextHtml);
}
} else if (propKey === CHILDREN) {
if (typeof nextProp === 'string') {
// Avoid setting initial textContent when the text is empty. In IE11 setting
// textContent on a <textarea> will cause the placeholder to not
// show within the <textarea> until it has been focused and blurred again.
// https://github.com/facebook/react/issues/6731#issuecomment-254874553
const canSetTextContent = tag !== 'textarea' || nextProp !== '';
if (canSetTextContent) {
setTextContent(domElement, nextProp);
}
} else if (typeof nextProp === 'number') {
setTextContent(domElement, '' + nextProp);
}
} else if (
(enableDeprecatedFlareAPI && propKey === DEPRECATED_flareListeners) ||
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
} else if (propKey === 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).
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey);
}
} else if (nextProp != null) {
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
}
}
}
function updateDOMProperties(
domElement: Element,
updatePayload: Array<any>,
wasCustomComponentTag: boolean,
isCustomComponentTag: boolean,
): void {
// TODO: Handle wasCustomComponentTag
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if (propKey === STYLE) {
setValueForStyles(domElement, propValue);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue);
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}
export function createElement(
type: string,
props: Object,
rootContainerElement: Element | Document,
parentNamespace: string,
): Element {
let isCustomComponentTag;
// We create tags in the namespace of their parent container, except HTML
// tags get no namespace.
const ownerDocument: Document = getOwnerDocumentFromRootContainer(
rootContainerElement,
);
let domElement: Element;
let namespaceURI = parentNamespace;
if (namespaceURI === HTML_NAMESPACE) {
namespaceURI = getIntrinsicNamespace(type);
}
if (namespaceURI === HTML_NAMESPACE) {
if (__DEV__) {
isCustomComponentTag = isCustomComponent(type, props);
// Should this check be gated by parent namespace? Not sure we want to
// allow <SVG> or <mATH>.
if (!isCustomComponentTag && type !== type.toLowerCase()) {
console.error(
'<%s /> is using incorrect casing. ' +
'Use PascalCase for React components, ' +
'or lowercase for HTML elements.',
type,
);
}
}
if (type === 'script') {
// Create the script via .innerHTML so its "parser-inserted" flag is
// set to true and it does not execute
const div = ownerDocument.createElement('div');
if (__DEV__) {
if (enableTrustedTypesIntegration && !didWarnScriptTags) {
console.error(
'Encountered a script tag while rendering React component. ' +
'Scripts inside React components are never executed when rendering ' +
'on the client. Consider using template tag instead ' +
'(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).',
);
didWarnScriptTags = true;
}
}
div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
// This is guaranteed to yield a script element.
const firstChild = ((div.firstChild: any): HTMLScriptElement);
domElement = div.removeChild(firstChild);
} else if (typeof props.is === 'string') {
// $FlowIssue `createElement` should be updated for Web Components
domElement = ownerDocument.createElement(type, {is: props.is});
} else {
// Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
// See discussion in https://github.com/facebook/react/pull/6896
// and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
domElement = ownerDocument.createElement(type);
// Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` and `size`
// attributes on `select`s needs to be added before `option`s are inserted.
// This prevents:
// - a bug where the `select` does not scroll to the correct option because singular
// `select` elements automatically pick the first item #13222
// - a bug where the `select` set the first item as selected despite the `size` attribute #14239
// See https://github.com/facebook/react/issues/13222
// and https://github.com/facebook/react/issues/14239
if (type === 'select') {
const node = ((domElement: any): HTMLSelectElement);
if (props.multiple) {
node.multiple = true;
} else if (props.size) {
// Setting a size greater than 1 causes a select to behave like `multiple=true`, where
// it is possible that no option is selected.
//
// This is only necessary when a select in "single selection mode".
node.size = props.size;
}
}
}
} else {
domElement = ownerDocument.createElementNS(namespaceURI, type);
}
if (__DEV__) {
if (namespaceURI === HTML_NAMESPACE) {
if (
!isCustomComponentTag &&
Object.prototype.toString.call(domElement) ===
'[object HTMLUnknownElement]' &&
!Object.prototype.hasOwnProperty.call(warnedUnknownTags, type)
) {
warnedUnknownTags[type] = true;
console.error(
'The tag <%s> is unrecognized in this browser. ' +
'If you meant to render a React component, start its name with ' +
'an uppercase letter.',
type,
);
}
}
}
return domElement;
}
export function createTextNode(
text: string,
rootContainerElement: Element | Document,
): Text {
return getOwnerDocumentFromRootContainer(rootContainerElement).createTextNode(
text,
);
}
export function setInitialProperties(
domElement: Element,
tag: string,
rawProps: Object,
rootContainerElement: Element | Document,
): void {
const isCustomComponentTag = isCustomComponent(tag, rawProps);
if (__DEV__) {
validatePropertiesInDevelopment(tag, rawProps);
}
// TODO: Make sure that we check isMounted before firing any of these events.
let props: Object;
switch (tag) {
case 'iframe':
case 'object':
case 'embed':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_LOAD, domElement);
}
props = rawProps;
break;
case 'video':
case 'audio':
if (!enableModernEventSystem) {
// Create listener for each media event
for (let i = 0; i < mediaEventTypes.length; i++) {
legacyTrapBubbledEvent(mediaEventTypes[i], domElement);
}
}
props = rawProps;
break;
case 'source':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_ERROR, domElement);
}
props = rawProps;
break;
case 'img':
case 'image':
case 'link':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_ERROR, domElement);
legacyTrapBubbledEvent(TOP_LOAD, domElement);
}
props = rawProps;
break;
case 'form':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_RESET, domElement);
legacyTrapBubbledEvent(TOP_SUBMIT, domElement);
}
props = rawProps;
break;
case 'details':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_TOGGLE, domElement);
}
props = rawProps;
break;
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
props = ReactDOMInputGetHostProps(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'option':
ReactDOMOptionValidateProps(domElement, rawProps);
props = ReactDOMOptionGetHostProps(domElement, rawProps);
break;
case 'select':
ReactDOMSelectInitWrapperState(domElement, rawProps);
props = ReactDOMSelectGetHostProps(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
props = ReactDOMTextareaGetHostProps(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
default:
props = rawProps;
}
assertValidProps(tag, props);
setInitialDOMProperties(
tag,
domElement,
rootContainerElement,
props,
isCustomComponentTag,
);
switch (tag) {
case 'input':
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
track((domElement: any));
ReactDOMInputPostMountWrapper(domElement, rawProps, false);
break;
case 'textarea':
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
track((domElement: any));
ReactDOMTextareaPostMountWrapper(domElement, rawProps);
break;
case 'option':
ReactDOMOptionPostMountWrapper(domElement, rawProps);
break;
case 'select':
ReactDOMSelectPostMountWrapper(domElement, rawProps);
break;
default:
if (typeof props.onClick === 'function') {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
}
break;
}
}
// Calculate the diff between the two objects.
export function diffProperties(
domElement: Element,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
rootContainerElement: Element | Document,
): null | Array<mixed> {
if (__DEV__) {
validatePropertiesInDevelopment(tag, nextRawProps);
}
let updatePayload: null | Array<any> = null;
let lastProps: Object;
let nextProps: Object;
switch (tag) {
case 'input':
lastProps = ReactDOMInputGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMInputGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case 'option':
lastProps = ReactDOMOptionGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMOptionGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case 'select':
lastProps = ReactDOMSelectGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMSelectGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case 'textarea':
lastProps = ReactDOMTextareaGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMTextareaGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
default:
lastProps = lastRawProps;
nextProps = nextRawProps;
if (
typeof lastProps.onClick !== 'function' &&
typeof nextProps.onClick === 'function'
) {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
}
break;
}
assertValidProps(tag, nextProps);
let propKey;
let styleName;
let styleUpdates = null;
for (propKey in lastProps) {
if (
nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey) ||
lastProps[propKey] == null
) {
continue;
}
if (propKey === STYLE) {
const lastStyle = lastProps[propKey];
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = '';
}
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) {
// Noop. This is handled by the clear text mechanism.
} else if (
(enableDeprecatedFlareAPI && propKey === DEPRECATED_flareListeners) ||
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
} else if (propKey === AUTOFOCUS) {
// Noop. It doesn't work on updates anyway.
} else if (registrationNameModules.hasOwnProperty(propKey)) {
// This is a special case. If any listener updates we need to ensure
// that the "current" fiber pointer gets updated so we need a commit
// to update this element.
if (!updatePayload) {
updatePayload = [];
}
} else {
// For all other deleted properties we add it to the queue. We use
// the allowed property list in the commit phase instead.
(updatePayload = updatePayload || []).push(propKey, null);
}
}
for (propKey in nextProps) {
const nextProp = nextProps[propKey];
const lastProp = lastProps != null ? lastProps[propKey] : undefined;
if (
!nextProps.hasOwnProperty(propKey) ||
nextProp === lastProp ||
(nextProp == null && lastProp == null)
) {
continue;
}
if (propKey === STYLE) {
if (__DEV__) {
if (nextProp) {
// Freeze the next style object so that we can assume it won't be
// mutated. We have already warned for this in the past.
Object.freeze(nextProp);
}
}
if (lastProp) {
// Unset styles on `lastProp` but not on `nextProp`.
for (styleName in lastProp) {
if (
lastProp.hasOwnProperty(styleName) &&
(!nextProp || !nextProp.hasOwnProperty(styleName))
) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = '';
}
}
// Update styles that changed since `lastProp`.
for (styleName in nextProp) {
if (
nextProp.hasOwnProperty(styleName) &&
lastProp[styleName] !== nextProp[styleName]
) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
if (!styleUpdates) {
if (!updatePayload) {
updatePayload = [];
}
updatePayload.push(propKey, styleUpdates);
}
styleUpdates = nextProp;
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
const nextHtml = nextProp ? nextProp[HTML] : undefined;
const lastHtml = lastProp ? lastProp[HTML] : undefined;
if (nextHtml != null) {
if (lastHtml !== nextHtml) {
(updatePayload = updatePayload || []).push(propKey, nextHtml);
}
} else {
// TODO: It might be too late to clear this if we have children
// inserted already.
}
} else if (propKey === CHILDREN) {
if (typeof nextProp === 'string' || typeof nextProp === 'number') {
(updatePayload = updatePayload || []).push(propKey, '' + nextProp);
}
} else if (
(enableDeprecatedFlareAPI && propKey === DEPRECATED_flareListeners) ||
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
// We eagerly listen to this even though we haven't committed yet.
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey);
}
if (!updatePayload && lastProp !== nextProp) {
// This is a special case. If any listener updates we need to ensure
// that the "current" props pointer gets updated so we need a commit
// to update this element.
updatePayload = [];
}
} else if (
typeof nextProp === 'object' &&
nextProp !== null &&
nextProp.$$typeof === REACT_OPAQUE_ID_TYPE
) {
// If we encounter useOpaqueReference's opaque object, this means we are hydrating.
// In this case, call the opaque object's toString function which generates a new client
// ID so client and server IDs match and throws to rerender.
nextProp.toString();
} else {
// For any other property we always add it to the queue and then we
// filter it out using the allowed property list during the commit.
(updatePayload = updatePayload || []).push(propKey, nextProp);
}
}
if (styleUpdates) {
if (__DEV__) {
validateShorthandPropertyCollisionInDev(styleUpdates, nextProps[STYLE]);
}
(updatePayload = updatePayload || []).push(STYLE, styleUpdates);
}
return updatePayload;
}
// Apply the diff.
export function updateProperties(
domElement: Element,
updatePayload: Array<any>,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
): void {
// 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.
if (
tag === 'input' &&
nextRawProps.type === 'radio' &&
nextRawProps.name != null
) {
ReactDOMInputUpdateChecked(domElement, nextRawProps);
}
const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
// Apply the diff.
updateDOMProperties(
domElement,
updatePayload,
wasCustomComponentTag,
isCustomComponentTag,
);
// TODO: Ensure that an update gets scheduled if any of the special props
// changed.
switch (tag) {
case 'input':
// Update the wrapper around inputs *after* updating props. This has to
// happen after `updateDOMProperties`. Otherwise HTML5 input validations
// raise warnings and prevent the new value from being assigned.
ReactDOMInputUpdateWrapper(domElement, nextRawProps);
break;
case 'textarea':
ReactDOMTextareaUpdateWrapper(domElement, nextRawProps);
break;
case 'select':
// <select> value update needs to occur after <option> children
// reconciliation
ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps);
break;
}
}
function getPossibleStandardName(propName: string): string | null {
if (__DEV__) {
const lowerCasedName = propName.toLowerCase();
if (!possibleStandardNames.hasOwnProperty(lowerCasedName)) {
return null;
}
return possibleStandardNames[lowerCasedName] || null;
}
return null;
}
export function diffHydratedProperties(
domElement: Element,
tag: string,
rawProps: Object,
parentNamespace: string,
rootContainerElement: Element | Document,
): null | Array<mixed> {
let isCustomComponentTag;
let extraAttributeNames: Set<string>;
if (__DEV__) {
suppressHydrationWarning = rawProps[SUPPRESS_HYDRATION_WARNING] === true;
isCustomComponentTag = isCustomComponent(tag, rawProps);
validatePropertiesInDevelopment(tag, rawProps);
}
// TODO: Make sure that we check isMounted before firing any of these events.
switch (tag) {
case 'iframe':
case 'object':
case 'embed':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_LOAD, domElement);
}
break;
case 'video':
case 'audio':
if (!enableModernEventSystem) {
// Create listener for each media event
for (let i = 0; i < mediaEventTypes.length; i++) {
legacyTrapBubbledEvent(mediaEventTypes[i], domElement);
}
}
break;
case 'source':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_ERROR, domElement);
}
break;
case 'img':
case 'image':
case 'link':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_ERROR, domElement);
legacyTrapBubbledEvent(TOP_LOAD, domElement);
}
break;
case 'form':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_RESET, domElement);
legacyTrapBubbledEvent(TOP_SUBMIT, domElement);
}
break;
case 'details':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_TOGGLE, domElement);
}
break;
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'option':
ReactDOMOptionValidateProps(domElement, rawProps);
break;
case 'select':
ReactDOMSelectInitWrapperState(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
}
assertValidProps(tag, rawProps);
if (__DEV__) {
extraAttributeNames = new Set();
const attributes = domElement.attributes;
for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name.toLowerCase();
switch (name) {
// Built-in SSR attribute is allowed
case 'data-reactroot':
break;
// Controlled attributes are not validated
// TODO: Only ignore them on controlled tags.
case 'value':
break;
case 'checked':
break;
case 'selected':
break;
default:
// Intentionally use the original name.
// See discussion in https://github.com/facebook/react/pull/10676.
extraAttributeNames.add(attributes[i].name);
}
}
}
let updatePayload = null;
for (const propKey in rawProps) {
if (!rawProps.hasOwnProperty(propKey)) {
continue;
}
const nextProp = rawProps[propKey];
if (propKey === CHILDREN) {
// For text content children we compare against textContent. This
// might match additional HTML that is hidden when we read it using
// textContent. E.g. "foo" will match "f<span>oo</span>" but that still
// satisfies our requirement. Our requirement is not to produce perfect
// HTML and attributes. Ideally we should preserve structure but it's
// ok not to if the visible content is still enough to indicate what
// even listeners these nodes might be wired up to.
// TODO: Warn if there is more than a single textNode as a child.
// TODO: Should we use domElement.firstChild.nodeValue to compare?
if (typeof nextProp === 'string') {
if (domElement.textContent !== nextProp) {
if (__DEV__ && !suppressHydrationWarning) {
warnForTextDifference(domElement.textContent, nextProp);
}
updatePayload = [CHILDREN, nextProp];
}
} else if (typeof nextProp === 'number') {
if (domElement.textContent !== '' + nextProp) {
if (__DEV__ && !suppressHydrationWarning) {
warnForTextDifference(domElement.textContent, nextProp);
}
updatePayload = [CHILDREN, '' + nextProp];
}
}
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
ensureListeningTo(rootContainerElement, propKey);
}
} else if (
__DEV__ &&
// Convince Flow we've calculated it (it's DEV-only in this method.)
typeof isCustomComponentTag === 'boolean'
) {
// Validate that the properties correspond to their expected values.
let serverValue;
const propertyInfo = getPropertyInfo(propKey);
if (suppressHydrationWarning) {
// Don't bother comparing. We're ignoring all these warnings.
} else if (
(enableDeprecatedFlareAPI && propKey === DEPRECATED_flareListeners) ||
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING ||
// Controlled attributes are not validated
// TODO: Only ignore them on controlled tags.
propKey === 'value' ||
propKey === 'checked' ||
propKey === 'selected'
) {
// Noop
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
const serverHTML = domElement.innerHTML;
const nextHtml = nextProp ? nextProp[HTML] : undefined;
if (nextHtml != null) {
const expectedHTML = normalizeHTML(domElement, nextHtml);
if (expectedHTML !== serverHTML) {
warnForPropDifference(propKey, serverHTML, expectedHTML);
}
}
} else if (propKey === STYLE) {
// $FlowFixMe - Should be inferred as not undefined.
extraAttributeNames.delete(propKey);
if (canDiffStyleForHydrationWarning) {
const expectedStyle = createDangerousStringForStyles(nextProp);
serverValue = domElement.getAttribute('style');
if (expectedStyle !== serverValue) {
warnForPropDifference(propKey, serverValue, expectedStyle);
}
}
} else if (isCustomComponentTag) {
// $FlowFixMe - Should be inferred as not undefined.
extraAttributeNames.delete(propKey.toLowerCase());
serverValue = getValueForAttribute(domElement, propKey, nextProp);
if (nextProp !== serverValue) {
warnForPropDifference(propKey, serverValue, nextProp);
}
} else if (
!shouldIgnoreAttribute(propKey, propertyInfo, isCustomComponentTag) &&
!shouldRemoveAttribute(
propKey,
nextProp,
propertyInfo,
isCustomComponentTag,
)
) {
let isMismatchDueToBadCasing = false;
if (propertyInfo !== null) {
// $FlowFixMe - Should be inferred as not undefined.
extraAttributeNames.delete(propertyInfo.attributeName);
serverValue = getValueForProperty(
domElement,
propKey,
nextProp,
propertyInfo,
);
} else {
let ownNamespace = parentNamespace;
if (ownNamespace === HTML_NAMESPACE) {
ownNamespace = getIntrinsicNamespace(tag);
}
if (ownNamespace === HTML_NAMESPACE) {
// $FlowFixMe - Should be inferred as not undefined.
extraAttributeNames.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;
// $FlowFixMe - Should be inferred as not undefined.
extraAttributeNames.delete(standardName);
}
// $FlowFixMe - Should be inferred as not undefined.
extraAttributeNames.delete(propKey);
}
serverValue = getValueForAttribute(domElement, propKey, nextProp);
}
if (nextProp !== serverValue && !isMismatchDueToBadCasing) {
warnForPropDifference(propKey, serverValue, nextProp);
}
}
}
}
if (__DEV__) {
// $FlowFixMe - Should be inferred as not undefined.
if (extraAttributeNames.size > 0 && !suppressHydrationWarning) {
// $FlowFixMe - Should be inferred as not undefined.
warnForExtraAttributes(extraAttributeNames);
}
}
switch (tag) {
case 'input':
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
track((domElement: any));
ReactDOMInputPostMountWrapper(domElement, rawProps, true);
break;
case 'textarea':
// TODO: Make sure we check if this is still unmounted or do any clean
// up necessary since we never stop tracking anymore.
track((domElement: any));
ReactDOMTextareaPostMountWrapper(domElement, rawProps);
break;
case 'select':
case 'option':
// For input and textarea we current always set the value property at
// post mount to force it to diverge from attributes. However, for
// option and select we don't quite do the same thing and select
// is not resilient to the DOM state changing so we don't do that here.
// TODO: Consider not doing this for input and textarea.
break;
default:
if (typeof rawProps.onClick === 'function') {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
}
break;
}
return updatePayload;
}
export function diffHydratedText(textNode: Text, text: string): boolean {
const isDifferent = textNode.nodeValue !== text;
return isDifferent;
}
export function warnForUnmatchedText(textNode: Text, text: string) {
if (__DEV__) {
warnForTextDifference(textNode.nodeValue, text);
}
}
export function warnForDeletedHydratableElement(
parentNode: Element | Document,
child: Element,
) {
if (__DEV__) {
if (didWarnInvalidHydration) {
return;
}
didWarnInvalidHydration = true;
console.error(
'Did not expect server HTML to contain a <%s> in <%s>.',
child.nodeName.toLowerCase(),
parentNode.nodeName.toLowerCase(),
);
}
}
export function warnForDeletedHydratableText(
parentNode: Element | Document,
child: Text,
) {
if (__DEV__) {
if (didWarnInvalidHydration) {
return;
}
didWarnInvalidHydration = true;
console.error(
'Did not expect server HTML to contain the text node "%s" in <%s>.',
child.nodeValue,
parentNode.nodeName.toLowerCase(),
);
}
}
export function warnForInsertedHydratedElement(
parentNode: Element | Document,
tag: string,
props: Object,
) {
if (__DEV__) {
if (didWarnInvalidHydration) {
return;
}
didWarnInvalidHydration = true;
console.error(
'Expected server HTML to contain a matching <%s> in <%s>.',
tag,
parentNode.nodeName.toLowerCase(),
);
}
}
export function warnForInsertedHydratedText(
parentNode: Element | Document,
text: string,
) {
if (__DEV__) {
if (text === '') {
// We expect to insert empty text nodes since they're not represented in
// the HTML.
// TODO: Remove this special case if we can just avoid inserting empty
// text nodes.
return;
}
if (didWarnInvalidHydration) {
return;
}
didWarnInvalidHydration = true;
console.error(
'Expected server HTML to contain a matching text node for "%s" in <%s>.',
text,
parentNode.nodeName.toLowerCase(),
);
}
}
export function restoreControlledState(
domElement: Element,
tag: string,
props: Object,
): void {
switch (tag) {
case 'input':
ReactDOMInputRestoreControlledState(domElement, props);
return;
case 'textarea':
ReactDOMTextareaRestoreControlledState(domElement, props);
return;
case 'select':
ReactDOMSelectRestoreControlledState(domElement, props);
return;
}
}
function endsWith(subject: string, search: string): boolean {
const length = subject.length;
return subject.substring(length - search.length, length) === search;
}
export function listenToEventResponderEventTypes(
eventTypes: Array<string>,
document: Document,
): void {
if (enableDeprecatedFlareAPI) {
// Get the listening Map for this element. We use this to track
// what events we're listening to.
const listenerMap = getEventListenerMap(document);
// Go through each target event type of the event responder
for (let i = 0, length = eventTypes.length; i < length; ++i) {
const eventType = eventTypes[i];
const isPassive = !endsWith(eventType, '_active');
const eventKey = isPassive ? eventType + '_passive' : eventType;
const targetEventType = isPassive
? eventType
: eventType.substring(0, eventType.length - 7);
if (!listenerMap.has(eventKey)) {
if (isPassive) {
const activeKey = targetEventType + '_active';
// If we have an active event listener, do not register
// a passive event listener. We use the same active event
// listener.
if (listenerMap.has(activeKey)) {
continue;
}
} else {
// If we have a passive event listener, remove the
// existing passive event listener before we add the
// active event listener.
const passiveKey = targetEventType + '_passive';
const passiveItem = listenerMap.get(passiveKey);
if (passiveItem !== undefined) {
removeTrappedEventListener(
document,
(targetEventType: any),
true,
passiveItem.listener,
);
listenerMap.delete(passiveKey);
}
}
const eventListener = addResponderEventSystemEvent(
document,
targetEventType,
isPassive,
);
listenerMap.set(eventKey, {
passive: isPassive,
listener: eventListener,
});
}
}
}
}
// We can remove this once the event API is stable and out of a flag
if (enableDeprecatedFlareAPI) {
setListenToResponderEventTypes(listenToEventResponderEventTypes);
}