diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index 68f0e5b39a..622b8e0449 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -80,7 +80,6 @@ import { let didWarnControlledToUncontrolled = false; let didWarnUncontrolledToControlled = false; -let didWarnInvalidHydration = false; let didWarnFormActionType = false; let didWarnFormActionName = false; let didWarnFormActionTarget = false; @@ -227,11 +226,9 @@ function warnForPropDifference( propName: string, serverValue: mixed, clientValue: mixed, -) { + serverDifferences: {[propName: string]: mixed}, +): void { if (__DEV__) { - if (didWarnInvalidHydration) { - return; - } if (serverValue === clientValue) { return; } @@ -242,27 +239,23 @@ function warnForPropDifference( if (normalizedServerValue === normalizedClientValue) { return; } - didWarnInvalidHydration = true; - console.error( - 'Prop `%s` did not match. Server: %s Client: %s', - propName, - JSON.stringify(normalizedServerValue), - JSON.stringify(normalizedClientValue), - ); + + serverDifferences[propName] = serverValue; } } -function warnForExtraAttributes(attributeNames: Set) { +function warnForExtraAttributes( + domElement: Element, + attributeNames: Set, + serverDifferences: {[propName: string]: mixed}, +) { if (__DEV__) { - if (didWarnInvalidHydration) { - return; - } - didWarnInvalidHydration = true; - const names = []; - attributeNames.forEach(function (name) { - names.push(name); + attributeNames.forEach(function (attributeName) { + serverDifferences[attributeName] = + attributeName === 'style' + ? getStylesObjectFromElement(domElement) + : domElement.getAttribute(attributeName); }); - console.error('Extra attributes from the server: %s', names); } } @@ -326,33 +319,16 @@ function normalizeMarkupForTextOrAttribute(markup: mixed): string { .replace(NORMALIZE_NULL_AND_REPLACEMENT_REGEX, ''); } -export function checkForUnmatchedText( +function checkForUnmatchedText( serverText: string, clientText: string | number | bigint, - shouldWarnDev: boolean, ) { const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText); const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText); if (normalizedServerText === normalizedClientText) { - return; + return true; } - - if (shouldWarnDev) { - if (__DEV__) { - if (!didWarnInvalidHydration) { - didWarnInvalidHydration = true; - console.error( - 'Text content did not match. Server: "%s" Client: "%s"', - normalizedServerText, - normalizedClientText, - ); - } - } - } - - // In concurrent roots, we throw when there's a text mismatch and revert to - // client rendering, up to the nearest Suspense boundary. - throw new Error('Text content does not match server-rendered HTML.'); + return false; } function noop() {} @@ -1853,18 +1829,69 @@ function getPossibleStandardName(propName: string): string | null { return null; } -function diffHydratedStyles(domElement: Element, value: mixed) { +export function getPropsFromElement(domElement: Element): Object { + const serverDifferences: {[propName: string]: mixed} = {}; + const attributes = domElement.attributes; + for (let i = 0; i < attributes.length; i++) { + const attr = attributes[i]; + serverDifferences[attr.name] = + attr.name.toLowerCase() === 'style' + ? getStylesObjectFromElement(domElement) + : attr.value; + } + return serverDifferences; +} + +function getStylesObjectFromElement(domElement: Element): { + [styleName: string]: string, +} { + const serverValueInObjectForm: {[prop: string]: string} = {}; + const style = ((domElement: any): HTMLElement).style; + for (let i = 0; i < style.length; i++) { + const styleName: string = style[i]; + // TODO: We should use the original prop value here if it is equivalent. + // TODO: We could use the original client capitalization if the equivalent + // other capitalization exists in the DOM. + serverValueInObjectForm[styleName] = style.getPropertyValue(styleName); + } + return serverValueInObjectForm; +} + +function diffHydratedStyles( + domElement: Element, + value: mixed, + serverDifferences: {[propName: string]: mixed}, +): void { if (value != null && typeof value !== 'object') { - throw new Error( - 'The `style` prop expects a mapping from style properties to values, ' + - "not a string. For example, style={{marginRight: spacing + 'em'}} when " + - 'using JSX.', - ); + if (__DEV__) { + console.error( + 'The `style` prop expects a mapping from style properties to values, ' + + "not a string. For example, style={{marginRight: spacing + 'em'}} when " + + 'using JSX.', + ); + } + return; } if (canDiffStyleForHydrationWarning) { - const expectedStyle = createDangerousStringForStyles(value); + // First we compare the string form and see if it's equivalent. + // This lets us bail out on anything that used to pass in this form. + // It also lets us compare anything that's not parsed by this browser. + const clientValue = createDangerousStringForStyles(value); const serverValue = domElement.getAttribute('style'); - warnForPropDifference('style', serverValue, expectedStyle); + + if (serverValue === clientValue) { + return; + } + const normalizedClientValue = + normalizeMarkupForTextOrAttribute(clientValue); + const normalizedServerValue = + normalizeMarkupForTextOrAttribute(serverValue); + if (normalizedServerValue === normalizedClientValue) { + return; + } + + // Otherwise, we create the object from the DOM for the diff view. + serverDifferences.style = getStylesObjectFromElement(domElement); } } @@ -1874,6 +1901,7 @@ function hydrateAttribute( attributeName: string, value: any, extraAttributes: Set, + serverDifferences: {[propName: string]: mixed}, ): void { extraAttributes.delete(attributeName); const serverValue = domElement.getAttribute(attributeName); @@ -1906,7 +1934,7 @@ function hydrateAttribute( } } } - warnForPropDifference(propKey, serverValue, value); + warnForPropDifference(propKey, serverValue, value, serverDifferences); } function hydrateBooleanAttribute( @@ -1915,6 +1943,7 @@ function hydrateBooleanAttribute( attributeName: string, value: any, extraAttributes: Set, + serverDifferences: {[propName: string]: mixed}, ): void { extraAttributes.delete(attributeName); const serverValue = domElement.getAttribute(attributeName); @@ -1942,7 +1971,7 @@ function hydrateBooleanAttribute( } } } - warnForPropDifference(propKey, serverValue, value); + warnForPropDifference(propKey, serverValue, value, serverDifferences); } function hydrateOverloadedBooleanAttribute( @@ -1951,6 +1980,7 @@ function hydrateOverloadedBooleanAttribute( attributeName: string, value: any, extraAttributes: Set, + serverDifferences: {[propName: string]: mixed}, ): void { extraAttributes.delete(attributeName); const serverValue = domElement.getAttribute(attributeName); @@ -1990,7 +2020,7 @@ function hydrateOverloadedBooleanAttribute( } } } - warnForPropDifference(propKey, serverValue, value); + warnForPropDifference(propKey, serverValue, value, serverDifferences); } function hydrateBooleanishAttribute( @@ -1999,6 +2029,7 @@ function hydrateBooleanishAttribute( attributeName: string, value: any, extraAttributes: Set, + serverDifferences: {[propName: string]: mixed}, ): void { extraAttributes.delete(attributeName); const serverValue = domElement.getAttribute(attributeName); @@ -2029,7 +2060,7 @@ function hydrateBooleanishAttribute( } } } - warnForPropDifference(propKey, serverValue, value); + warnForPropDifference(propKey, serverValue, value, serverDifferences); } function hydrateNumericAttribute( @@ -2038,6 +2069,7 @@ function hydrateNumericAttribute( attributeName: string, value: any, extraAttributes: Set, + serverDifferences: {[propName: string]: mixed}, ): void { extraAttributes.delete(attributeName); const serverValue = domElement.getAttribute(attributeName); @@ -2079,7 +2111,7 @@ function hydrateNumericAttribute( } } } - warnForPropDifference(propKey, serverValue, value); + warnForPropDifference(propKey, serverValue, value, serverDifferences); } function hydratePositiveNumericAttribute( @@ -2088,6 +2120,7 @@ function hydratePositiveNumericAttribute( attributeName: string, value: any, extraAttributes: Set, + serverDifferences: {[propName: string]: mixed}, ): void { extraAttributes.delete(attributeName); const serverValue = domElement.getAttribute(attributeName); @@ -2129,7 +2162,7 @@ function hydratePositiveNumericAttribute( } } } - warnForPropDifference(propKey, serverValue, value); + warnForPropDifference(propKey, serverValue, value, serverDifferences); } function hydrateSanitizedAttribute( @@ -2138,6 +2171,7 @@ function hydrateSanitizedAttribute( attributeName: string, value: any, extraAttributes: Set, + serverDifferences: {[propName: string]: mixed}, ): void { extraAttributes.delete(attributeName); const serverValue = domElement.getAttribute(attributeName); @@ -2171,7 +2205,7 @@ function hydrateSanitizedAttribute( } } } - warnForPropDifference(propKey, serverValue, value); + warnForPropDifference(propKey, serverValue, value, serverDifferences); } function diffHydratedCustomComponent( @@ -2180,6 +2214,7 @@ function diffHydratedCustomComponent( props: Object, hostContext: HostContext, extraAttributes: Set, + serverDifferences: {[propName: string]: mixed}, ) { for (const propKey in props) { if (!props.hasOwnProperty(propKey)) { @@ -2201,7 +2236,18 @@ function diffHydratedCustomComponent( } // Validate that the properties correspond to their expected values. switch (propKey) { - case 'children': // Checked above already + case 'children': { + if (typeof value === 'string' || typeof value === 'number') { + warnForPropDifference( + 'children', + domElement.textContent, + value, + serverDifferences, + ); + } + continue; + } + // Checked above already case 'suppressContentEditableWarning': case 'suppressHydrationWarning': case 'defaultValue': @@ -2215,12 +2261,17 @@ function diffHydratedCustomComponent( const nextHtml = value ? value.__html : undefined; if (nextHtml != null) { const expectedHTML = normalizeHTML(domElement, nextHtml); - warnForPropDifference(propKey, serverHTML, expectedHTML); + warnForPropDifference( + propKey, + serverHTML, + expectedHTML, + serverDifferences, + ); } continue; case 'style': extraAttributes.delete(propKey); - diffHydratedStyles(domElement, value); + diffHydratedStyles(domElement, value, serverDifferences); continue; case 'offsetParent': case 'offsetTop': @@ -2250,7 +2301,12 @@ function diffHydratedCustomComponent( 'class', value, ); - warnForPropDifference('className', serverValue, value); + warnForPropDifference( + 'className', + serverValue, + value, + serverDifferences, + ); continue; } // Fall through @@ -2272,7 +2328,7 @@ function diffHydratedCustomComponent( propKey, value, ); - warnForPropDifference(propKey, serverValue, value); + warnForPropDifference(propKey, serverValue, value, serverDifferences); } } } @@ -2291,6 +2347,7 @@ function diffHydratedGenericElement( props: Object, hostContext: HostContext, extraAttributes: Set, + serverDifferences: {[propName: string]: mixed}, ) { for (const propKey in props) { if (!props.hasOwnProperty(propKey)) { @@ -2312,7 +2369,18 @@ function diffHydratedGenericElement( } // Validate that the properties correspond to their expected values. switch (propKey) { - case 'children': // Checked above already + case 'children': { + if (typeof value === 'string' || typeof value === 'number') { + warnForPropDifference( + 'children', + domElement.textContent, + value, + serverDifferences, + ); + } + continue; + } + // Checked above already case 'suppressContentEditableWarning': case 'suppressHydrationWarning': case 'value': // Controlled attributes are not validated @@ -2329,11 +2397,22 @@ function diffHydratedGenericElement( const nextHtml = value ? value.__html : undefined; if (nextHtml != null) { const expectedHTML = normalizeHTML(domElement, nextHtml); - warnForPropDifference(propKey, serverHTML, expectedHTML); + if (serverHTML !== expectedHTML) { + serverDifferences[propKey] = { + __html: serverHTML, + }; + } } continue; case 'className': - hydrateAttribute(domElement, propKey, 'class', value, extraAttributes); + hydrateAttribute( + domElement, + propKey, + 'class', + value, + extraAttributes, + serverDifferences, + ); continue; case 'tabIndex': hydrateAttribute( @@ -2342,28 +2421,29 @@ function diffHydratedGenericElement( 'tabindex', value, extraAttributes, + serverDifferences, ); continue; case 'style': extraAttributes.delete(propKey); - diffHydratedStyles(domElement, value); + diffHydratedStyles(domElement, value, serverDifferences); continue; case 'multiple': { extraAttributes.delete(propKey); const serverValue = (domElement: any).multiple; - warnForPropDifference(propKey, serverValue, value); + warnForPropDifference(propKey, serverValue, value, serverDifferences); continue; } case 'muted': { extraAttributes.delete(propKey); const serverValue = (domElement: any).muted; - warnForPropDifference(propKey, serverValue, value); + warnForPropDifference(propKey, serverValue, value, serverDifferences); continue; } case 'autoFocus': { extraAttributes.delete('autofocus'); const serverValue = (domElement: any).autofocus; - warnForPropDifference(propKey, serverValue, value); + warnForPropDifference(propKey, serverValue, value, serverDifferences); continue; } case 'src': @@ -2400,6 +2480,7 @@ function diffHydratedGenericElement( propKey, null, extraAttributes, + serverDifferences, ); continue; } @@ -2410,6 +2491,7 @@ function diffHydratedGenericElement( propKey, value, extraAttributes, + serverDifferences, ); continue; case 'action': @@ -2438,7 +2520,7 @@ function diffHydratedGenericElement( continue; } else if (serverValue === EXPECTED_FORM_ACTION_URL) { extraAttributes.delete(propKey.toLowerCase()); - warnForPropDifference(propKey, 'function', value); + warnForPropDifference(propKey, 'function', value, serverDifferences); continue; } hydrateSanitizedAttribute( @@ -2447,6 +2529,7 @@ function diffHydratedGenericElement( propKey.toLowerCase(), value, extraAttributes, + serverDifferences, ); continue; } @@ -2457,6 +2540,7 @@ function diffHydratedGenericElement( 'xlink:href', value, extraAttributes, + serverDifferences, ); continue; case 'contentEditable': { @@ -2467,6 +2551,7 @@ function diffHydratedGenericElement( 'contenteditable', value, extraAttributes, + serverDifferences, ); continue; } @@ -2478,6 +2563,7 @@ function diffHydratedGenericElement( 'spellcheck', value, extraAttributes, + serverDifferences, ); continue; } @@ -2493,6 +2579,7 @@ function diffHydratedGenericElement( propKey, value, extraAttributes, + serverDifferences, ); continue; } @@ -2525,6 +2612,7 @@ function diffHydratedGenericElement( propKey.toLowerCase(), value, extraAttributes, + serverDifferences, ); continue; } @@ -2536,6 +2624,7 @@ function diffHydratedGenericElement( propKey, value, extraAttributes, + serverDifferences, ); continue; } @@ -2549,6 +2638,7 @@ function diffHydratedGenericElement( propKey, value, extraAttributes, + serverDifferences, ); continue; } @@ -2559,6 +2649,7 @@ function diffHydratedGenericElement( 'rowspan', value, extraAttributes, + serverDifferences, ); continue; } @@ -2569,6 +2660,7 @@ function diffHydratedGenericElement( propKey, value, extraAttributes, + serverDifferences, ); continue; } @@ -2579,6 +2671,7 @@ function diffHydratedGenericElement( 'x-height', value, extraAttributes, + serverDifferences, ); continue; case 'xlinkActuate': @@ -2588,6 +2681,7 @@ function diffHydratedGenericElement( 'xlink:actuate', value, extraAttributes, + serverDifferences, ); continue; case 'xlinkArcrole': @@ -2597,6 +2691,7 @@ function diffHydratedGenericElement( 'xlink:arcrole', value, extraAttributes, + serverDifferences, ); continue; case 'xlinkRole': @@ -2606,6 +2701,7 @@ function diffHydratedGenericElement( 'xlink:role', value, extraAttributes, + serverDifferences, ); continue; case 'xlinkShow': @@ -2615,6 +2711,7 @@ function diffHydratedGenericElement( 'xlink:show', value, extraAttributes, + serverDifferences, ); continue; case 'xlinkTitle': @@ -2624,6 +2721,7 @@ function diffHydratedGenericElement( 'xlink:title', value, extraAttributes, + serverDifferences, ); continue; case 'xlinkType': @@ -2633,6 +2731,7 @@ function diffHydratedGenericElement( 'xlink:type', value, extraAttributes, + serverDifferences, ); continue; case 'xmlBase': @@ -2642,6 +2741,7 @@ function diffHydratedGenericElement( 'xml:base', value, extraAttributes, + serverDifferences, ); continue; case 'xmlLang': @@ -2651,6 +2751,7 @@ function diffHydratedGenericElement( 'xml:lang', value, extraAttributes, + serverDifferences, ); continue; case 'xmlSpace': @@ -2660,6 +2761,7 @@ function diffHydratedGenericElement( 'xml:space', value, extraAttributes, + serverDifferences, ); continue; case 'inert': @@ -2685,6 +2787,7 @@ function diffHydratedGenericElement( propKey, value, extraAttributes, + serverDifferences, ); continue; } @@ -2731,20 +2834,19 @@ function diffHydratedGenericElement( value, ); if (!isMismatchDueToBadCasing) { - warnForPropDifference(propKey, serverValue, value); + warnForPropDifference(propKey, serverValue, value, serverDifferences); } } } } } -export function diffHydratedProperties( +export function hydrateProperties( domElement: Element, tag: string, props: Object, - shouldWarnDev: boolean, hostContext: HostContext, -): void { +): boolean { if (__DEV__) { validatePropertiesInDevelopment(tag, props); } @@ -2857,11 +2959,13 @@ export function diffHydratedProperties( typeof children === 'number' || (enableBigIntSupport && typeof children === 'bigint') ) { - // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint - if (domElement.textContent !== '' + children) { - if (props.suppressHydrationWarning !== true) { - checkForUnmatchedText(domElement.textContent, children, shouldWarnDev); - } + if ( + // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint + domElement.textContent !== '' + children && + props.suppressHydrationWarning !== true && + !checkForUnmatchedText(domElement.textContent, children) + ) { + return false; } } @@ -2878,7 +2982,17 @@ export function diffHydratedProperties( trapClickOnNonInteractiveElement(((domElement: any): HTMLElement)); } - if (__DEV__ && shouldWarnDev) { + return true; +} + +export function diffHydratedProperties( + domElement: Element, + tag: string, + props: Object, + hostContext: HostContext, +): null | Object { + const serverDifferences: {[propName: string]: mixed} = {}; + if (__DEV__) { const extraAttributes: Set = new Set(); const attributes = domElement.attributes; for (let i = 0; i < attributes.length; i++) { @@ -2905,6 +3019,7 @@ export function diffHydratedProperties( props, hostContext, extraAttributes, + serverDifferences, ); } else { diffHydratedGenericElement( @@ -2913,86 +3028,47 @@ export function diffHydratedProperties( props, hostContext, extraAttributes, + serverDifferences, ); } if (extraAttributes.size > 0 && props.suppressHydrationWarning !== true) { - warnForExtraAttributes(extraAttributes); + warnForExtraAttributes(domElement, extraAttributes, serverDifferences); } } -} - -export function diffHydratedText(textNode: Text, text: string): boolean { - const isDifferent = textNode.nodeValue !== text; - return isDifferent; -} - -export function warnForDeletedHydratableElement( - parentNode: Element | Document | DocumentFragment, - 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(), - ); + if (Object.keys(serverDifferences).length === 0) { + return null; } + return serverDifferences; } -export function warnForDeletedHydratableText( - parentNode: Element | Document | DocumentFragment, - 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 | DocumentFragment, - 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 | DocumentFragment, +export function hydrateText( + textNode: Text, text: string, -) { - if (__DEV__) { - if (didWarnInvalidHydration) { - return; - } - didWarnInvalidHydration = true; - console.error( - 'Expected server HTML to contain a matching text node for "%s" in <%s>.', - text, - parentNode.nodeName.toLowerCase(), - ); + parentProps: null | Object, +): boolean { + const isDifferent = textNode.nodeValue !== text; + if ( + isDifferent && + (parentProps === null || parentProps.suppressHydrationWarning !== true) && + !checkForUnmatchedText(textNode.nodeValue, text) + ) { + return false; } + return true; +} + +export function diffHydratedText(textNode: Text, text: string): null | string { + if (textNode.nodeValue === text) { + return null; + } + const normalizedClientText = normalizeMarkupForTextOrAttribute(text); + const normalizedServerText = normalizeMarkupForTextOrAttribute( + textNode.nodeValue, + ); + if (normalizedServerText === normalizedClientText) { + return null; + } + return textNode.nodeValue; } export function restoreControlledState( diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index a06f9c00b6..3953926509 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -52,14 +52,12 @@ import {hasRole} from './DOMAccessibilityRoles'; import { setInitialProperties, updateProperties, + hydrateProperties, + hydrateText, diffHydratedProperties, + getPropsFromElement, diffHydratedText, trapClickOnNonInteractiveElement, - checkForUnmatchedText, - warnForDeletedHydratableElement, - warnForDeletedHydratableText, - warnForInsertedHydratedElement, - warnForInsertedHydratedText, } from './ReactDOMComponent'; import {getSelectionInformation, restoreSelection} from './ReactInputSelection'; import setTextContent from './setTextContent'; @@ -1342,6 +1340,26 @@ export function getFirstHydratableChildWithinSuspenseInstance( return getNextHydratable(parentInstance.nextSibling); } +export function describeHydratableInstanceForDevWarnings( + instance: HydratableInstance, +): string | {type: string, props: $ReadOnly} { + // Reverse engineer a pseudo react-element from hydratable instnace + if (instance.nodeType === ELEMENT_NODE) { + // Reverse engineer a set of props that can print for dev warnings + return { + type: instance.nodeName.toLowerCase(), + props: getPropsFromElement((instance: any)), + }; + } else if (instance.nodeType === COMMENT_NODE) { + return { + type: 'Suspense', + props: {}, + }; + } else { + return instance.nodeValue; + } +} + export function validateHydratableInstance( type: string, props: Props, @@ -1361,14 +1379,23 @@ export function hydrateInstance( props: Props, hostContext: HostContext, internalInstanceHandle: Object, - shouldWarnDev: boolean, -): void { +): boolean { precacheFiberNode(internalInstanceHandle, instance); // TODO: Possibly defer this until the commit phase where all the events // get attached. updateFiberProps(instance, props); - diffHydratedProperties(instance, type, props, shouldWarnDev, hostContext); + return hydrateProperties(instance, type, props, hostContext); +} + +// Returns a Map of properties that were different on the server. +export function diffHydratedPropsForDevWarnings( + instance: Instance, + type: string, + props: Props, + hostContext: HostContext, +): null | $ReadOnly { + return diffHydratedProperties(instance, type, props, hostContext); } export function validateHydratableTextInstance( @@ -1389,11 +1416,26 @@ export function hydrateTextInstance( textInstance: TextInstance, text: string, internalInstanceHandle: Object, - shouldWarnDev: boolean, + parentInstanceProps: null | Props, ): boolean { precacheFiberNode(internalInstanceHandle, textInstance); - return diffHydratedText(textInstance, text); + return hydrateText(textInstance, text, parentInstanceProps); +} + +// Returns the server text if it differs from the client. +export function diffHydratedTextForDevWarnings( + textInstance: TextInstance, + text: string, + parentProps: null | Props, +): null | string { + if ( + parentProps === null || + parentProps[SUPPRESS_HYDRATION_WARNING] !== true + ) { + return diffHydratedText(textInstance, text); + } + return null; } export function hydrateSuspenseInstance( @@ -1485,183 +1527,6 @@ export function shouldDeleteUnhydratedTailInstances( return parentType !== 'form' && parentType !== 'button'; } -export function didNotMatchHydratedContainerTextInstance( - parentContainer: Container, - textInstance: TextInstance, - text: string, - shouldWarnDev: boolean, -) { - checkForUnmatchedText(textInstance.nodeValue, text, shouldWarnDev); -} - -export function didNotMatchHydratedTextInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - textInstance: TextInstance, - text: string, - shouldWarnDev: boolean, -) { - if (parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - checkForUnmatchedText(textInstance.nodeValue, text, shouldWarnDev); - } -} - -export function didNotHydrateInstanceWithinContainer( - parentContainer: Container, - instance: HydratableInstance, -) { - if (__DEV__) { - if (instance.nodeType === ELEMENT_NODE) { - warnForDeletedHydratableElement(parentContainer, (instance: any)); - } else if (instance.nodeType === COMMENT_NODE) { - // TODO: warnForDeletedHydratableSuspenseBoundary - } else { - warnForDeletedHydratableText(parentContainer, (instance: any)); - } - } -} - -export function didNotHydrateInstanceWithinSuspenseInstance( - parentInstance: SuspenseInstance, - instance: HydratableInstance, -) { - if (__DEV__) { - // $FlowFixMe[incompatible-type]: Only Element or Document can be parent nodes. - const parentNode: Element | Document | null = parentInstance.parentNode; - if (parentNode !== null) { - if (instance.nodeType === ELEMENT_NODE) { - warnForDeletedHydratableElement(parentNode, (instance: any)); - } else if (instance.nodeType === COMMENT_NODE) { - // TODO: warnForDeletedHydratableSuspenseBoundary - } else { - warnForDeletedHydratableText(parentNode, (instance: any)); - } - } - } -} - -export function didNotHydrateInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - instance: HydratableInstance, -) { - if (__DEV__) { - if (instance.nodeType === ELEMENT_NODE) { - warnForDeletedHydratableElement(parentInstance, (instance: any)); - } else if (instance.nodeType === COMMENT_NODE) { - // TODO: warnForDeletedHydratableSuspenseBoundary - } else { - warnForDeletedHydratableText(parentInstance, (instance: any)); - } - } -} - -export function didNotFindHydratableInstanceWithinContainer( - parentContainer: Container, - type: string, - props: Props, -) { - if (__DEV__) { - warnForInsertedHydratedElement(parentContainer, type, props); - } -} - -export function didNotFindHydratableTextInstanceWithinContainer( - parentContainer: Container, - text: string, -) { - if (__DEV__) { - warnForInsertedHydratedText(parentContainer, text); - } -} - -export function didNotFindHydratableSuspenseInstanceWithinContainer( - parentContainer: Container, -) { - if (__DEV__) { - // TODO: warnForInsertedHydratedSuspense(parentContainer); - } -} - -export function didNotFindHydratableInstanceWithinSuspenseInstance( - parentInstance: SuspenseInstance, - type: string, - props: Props, -) { - if (__DEV__) { - // $FlowFixMe[incompatible-type]: Only Element or Document can be parent nodes. - const parentNode: Element | Document | null = parentInstance.parentNode; - if (parentNode !== null) - warnForInsertedHydratedElement(parentNode, type, props); - } -} - -export function didNotFindHydratableTextInstanceWithinSuspenseInstance( - parentInstance: SuspenseInstance, - text: string, -) { - if (__DEV__) { - // $FlowFixMe[incompatible-type]: Only Element or Document can be parent nodes. - const parentNode: Element | Document | null = parentInstance.parentNode; - if (parentNode !== null) warnForInsertedHydratedText(parentNode, text); - } -} - -export function didNotFindHydratableSuspenseInstanceWithinSuspenseInstance( - parentInstance: SuspenseInstance, -) { - if (__DEV__) { - // const parentNode: Element | Document | null = parentInstance.parentNode; - // TODO: warnForInsertedHydratedSuspense(parentNode); - } -} - -export function didNotFindHydratableInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - type: string, - props: Props, -) { - if (__DEV__) { - warnForInsertedHydratedElement(parentInstance, type, props); - } -} - -export function didNotFindHydratableTextInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, - text: string, -) { - if (__DEV__) { - warnForInsertedHydratedText(parentInstance, text); - } -} - -export function didNotFindHydratableSuspenseInstance( - parentType: string, - parentProps: Props, - parentInstance: Instance, -) { - if (__DEV__) { - // TODO: warnForInsertedHydratedSuspense(parentInstance); - } -} - -export function errorHydratingContainer(parentContainer: Container): void { - if (__DEV__) { - // TODO: This gets logged by onRecoverableError, too, so we should be - // able to remove it. - console.error( - 'An error occurred during hydration. The server HTML was replaced with client content in <%s>.', - parentContainer.nodeName.toLowerCase(), - ); - } -} - // ------------------- // Test Selectors // ------------------- diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index b0945ba911..33b2b322de 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -2407,8 +2407,8 @@ describe('ReactDOMFizzServer', () => { ]); }).toErrorDev( [ - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in
.', - 'Warning: Expected server HTML to contain a matching
in
.\n' + + 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', + 'Warning: Expected server HTML to contain a matching
in the root.\n' + ' in div (at **)\n' + ' in App (at **)', ], @@ -2492,7 +2492,7 @@ describe('ReactDOMFizzServer', () => { }).toErrorDev( [ 'Warning: An error occurred during hydration. The server HTML was replaced with client content', - 'Warning: Expected server HTML to contain a matching
in
.\n' + + 'Warning: Expected server HTML to contain a matching
in the root.\n' + ' in div (at **)\n' + ' in App (at **)', ], @@ -6343,7 +6343,7 @@ describe('ReactDOMFizzServer', () => { await waitForAll([]); }).toErrorDev( [ - 'Expected server HTML to contain a matching in
', + 'Expected server HTML to contain a matching in the root', 'An error occurred during hydration', ], {withoutStack: 1}, diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js index d4cefa214c..5fa719e3d3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js @@ -248,7 +248,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { }).toErrorDev( [ 'Expected server HTML to contain a matching in ', - 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, ); @@ -337,7 +337,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { }).toErrorDev( [ 'Did not expect server HTML to contain the text node "Server" in ', - 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, ); @@ -385,7 +385,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { }).toErrorDev( [ 'Expected server HTML to contain a matching text node for "Client" in .', - 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, ); @@ -436,7 +436,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { }).toErrorDev( [ 'Did not expect server HTML to contain the text node "Server" in .', - 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, ); @@ -485,7 +485,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { }).toErrorDev( [ 'Expected server HTML to contain a matching text node for "Client" in .', - 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, ); @@ -608,7 +608,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { }).toErrorDev( [ 'Expected server HTML to contain a matching

in

.', - 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, ); @@ -654,7 +654,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { }).toErrorDev( [ 'Did not expect server HTML to contain a

in

.', - 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, ); diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 736b831702..fae926c622 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -6481,7 +6481,7 @@ body { }).toErrorDev( [ 'Warning: Text content did not match. Server: "server" Client: "client"', - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.', + 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, ); @@ -8271,7 +8271,7 @@ background-color: green; }).toErrorDev( [ 'Warning: Text content did not match. Server: "server" Client: "client"', - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.', + 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, ); diff --git a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js index 8a7cfb55d6..ef5507744b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js @@ -86,7 +86,7 @@ describe('ReactDOMServerHydration', () => { in main (at **) in div (at **) in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content in
.", + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", "Caught [Text content does not match server-rendered HTML.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] @@ -112,7 +112,7 @@ describe('ReactDOMServerHydration', () => { "Warning: Text content did not match. Server: "This markup contains an nbsp entity:   server text" Client: "This markup contains an nbsp entity:   client text" in div (at **) in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content in
.", + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", "Caught [Text content does not match server-rendered HTML.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] @@ -138,7 +138,7 @@ describe('ReactDOMServerHydration', () => { } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` [ - "Warning: Prop \`dangerouslySetInnerHTML\` did not match. Server: "server" Client: "client" + "Warning: Prop \`dangerouslySetInnerHTML\` did not match. Server: {"__html":"server"} Client: {"__html":"client"} in main (at **) in div (at **) in Mismatch (at **)", @@ -185,7 +185,7 @@ describe('ReactDOMServerHydration', () => { } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` [ - "Warning: Prop \`tabIndex\` did not match. Server: "null" Client: "1" + "Warning: Prop \`tabIndex\` did not match. Server: null Client: 1 in main (at **) in div (at **) in Mismatch (at **)", @@ -208,7 +208,7 @@ describe('ReactDOMServerHydration', () => { } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` [ - "Warning: Extra attributes from the server: tabindex,dir + "Warning: Extra attribute from the server: tabindex in main (at **) in div (at **) in Mismatch (at **)", @@ -231,7 +231,7 @@ describe('ReactDOMServerHydration', () => { } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` [ - "Warning: Prop \`tabIndex\` did not match. Server: "null" Client: "1" + "Warning: Prop \`tabIndex\` did not match. Server: null Client: 1 in main (at **) in div (at **) in Mismatch (at **)", @@ -255,7 +255,7 @@ describe('ReactDOMServerHydration', () => { } expect(testMismatch(Mismatch)).toMatchInlineSnapshot(` [ - "Warning: Prop \`style\` did not match. Server: "opacity:0" Client: "opacity:1" + "Warning: Prop \`style\` did not match. Server: {"opacity":"0"} Client: {"opacity":1} in main (at **) in div (at **) in Mismatch (at **)", @@ -281,7 +281,7 @@ describe('ReactDOMServerHydration', () => { in main (at **) in div (at **) in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content in
.", + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] @@ -305,7 +305,7 @@ describe('ReactDOMServerHydration', () => { in header (at **) in div (at **) in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content in
.", + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] @@ -329,7 +329,7 @@ describe('ReactDOMServerHydration', () => { in main (at **) in div (at **) in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content in
.", + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] @@ -353,7 +353,7 @@ describe('ReactDOMServerHydration', () => { in footer (at **) in div (at **) in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content in
.", + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] @@ -372,7 +372,7 @@ describe('ReactDOMServerHydration', () => { "Warning: Text content did not match. Server: "" Client: "only" in div (at **) in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content in
.", + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", "Caught [Text content does not match server-rendered HTML.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] @@ -395,7 +395,7 @@ describe('ReactDOMServerHydration', () => { "Warning: Expected server HTML to contain a matching text node for "second" in
. in div (at **) in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content in
.", + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] @@ -418,7 +418,7 @@ describe('ReactDOMServerHydration', () => { "Warning: Expected server HTML to contain a matching text node for "first" in
. in div (at **) in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content in
.", + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] @@ -441,7 +441,7 @@ describe('ReactDOMServerHydration', () => { "Warning: Expected server HTML to contain a matching text node for "third" in
. in div (at **) in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content in
.", + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] @@ -466,7 +466,7 @@ describe('ReactDOMServerHydration', () => { "Warning: Did not expect server HTML to contain a
in
. in div (at **) in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content in
.", + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] @@ -490,7 +490,7 @@ describe('ReactDOMServerHydration', () => { in main (at **) in div (at **) in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content in
.", + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] @@ -514,7 +514,7 @@ describe('ReactDOMServerHydration', () => { in footer (at **) in div (at **) in Mismatch (at **)", - "Warning: An error occurred during hydration. The server HTML was replaced with client content in
.", + "Warning: An error occurred during hydration. The server HTML was replaced with client content.", "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] @@ -537,7 +537,7 @@ describe('ReactDOMServerHydration', () => { "Warning: Did not expect server HTML to contain a