@samitouri / QOS-React-2 / commits / 4b8dfd6215

Move Hydration Warnings from the DOM Config into the Fiber reconciliation (#28476)

Stacked on #28458. This doesn't actually really change the messages yet, it's just a refactor. Hydration warnings can be presented either as HTML or React JSX format. If presented as HTML it makes more sense to make that a DOM specific concept, however, I think it's actually better to present it in terms of React JSX. Most of the time the errors aren't going to be something messing with them at the HTML/HTTP layer. It's because the JS code does something different. Most of the time you're working in just React. People don't necessarily even know what the HTML form of it looks like. So this takes the approach that the warnings are presented in React JSX in their rich object form. Therefore, I'm moving the approach to yield diff data to the reconciler but it's the reconciler that's actually printing all the warnings.

Sebastian Markbåge committed Mar 26, 2024 at 16:04 UTC 4b8dfd6215bf855402ae1a94cb0ae4f467afca9a
17 files changed +606 -581
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+222 -146
@@ -80,7 +80,6 @@ import {
80
81 let didWarnControlledToUncontrolled = false;
82 let didWarnUncontrolledToControlled = false;
83 -let didWarnInvalidHydration = false;
83 let didWarnFormActionType = false;
84 let didWarnFormActionName = false;
85 let didWarnFormActionTarget = false;
@@ -227,11 +226,9 @@ function warnForPropDifference(
226 propName: string,
227 serverValue: mixed,
228 clientValue: mixed,
230 -) {
229 + serverDifferences: {[propName: string]: mixed},
230 +): void {
231 if (__DEV__) {
232 - if (didWarnInvalidHydration) {
233 - return;
234 - }
232 if (serverValue === clientValue) {
233 return;
234 }
@@ -242,27 +239,23 @@ function warnForPropDifference(
239 if (normalizedServerValue === normalizedClientValue) {
240 return;
241 }
245 - didWarnInvalidHydration = true;
246 - console.error(
247 - 'Prop `%s` did not match. Server: %s Client: %s',
248 - propName,
249 - JSON.stringify(normalizedServerValue),
250 - JSON.stringify(normalizedClientValue),
251 - );
242 +
243 + serverDifferences[propName] = serverValue;
244 }
245 }
246
255 -function warnForExtraAttributes(attributeNames: Set<string>) {
247 +function warnForExtraAttributes(
248 + domElement: Element,
249 + attributeNames: Set<string>,
250 + serverDifferences: {[propName: string]: mixed},
251 +) {
252 if (__DEV__) {
257 - if (didWarnInvalidHydration) {
258 - return;
259 - }
260 - didWarnInvalidHydration = true;
261 - const names = [];
262 - attributeNames.forEach(function (name) {
263 - names.push(name);
253 + attributeNames.forEach(function (attributeName) {
254 + serverDifferences[attributeName] =
255 + attributeName === 'style'
256 + ? getStylesObjectFromElement(domElement)
257 + : domElement.getAttribute(attributeName);
258 });
265 - console.error('Extra attributes from the server: %s', names);
259 }
260 }
261
@@ -326,33 +319,16 @@ function normalizeMarkupForTextOrAttribute(markup: mixed): string {
319 .replace(NORMALIZE_NULL_AND_REPLACEMENT_REGEX, '');
320 }
321
329 -export function checkForUnmatchedText(
322 +function checkForUnmatchedText(
323 serverText: string,
324 clientText: string | number | bigint,
332 - shouldWarnDev: boolean,
325 ) {
326 const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText);
327 const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText);
328 if (normalizedServerText === normalizedClientText) {
337 - return;
338 - }
339 -
340 - if (shouldWarnDev) {
341 - if (__DEV__) {
342 - if (!didWarnInvalidHydration) {
343 - didWarnInvalidHydration = true;
344 - console.error(
345 - 'Text content did not match. Server: "%s" Client: "%s"',
346 - normalizedServerText,
347 - normalizedClientText,
348 - );
349 - }
350 - }
329 + return true;
330 }
352 -
353 - // In concurrent roots, we throw when there's a text mismatch and revert to
354 - // client rendering, up to the nearest Suspense boundary.
355 - throw new Error('Text content does not match server-rendered HTML.');
331 + return false;
332 }
333
334 function noop() {}
@@ -1853,18 +1829,69 @@ function getPossibleStandardName(propName: string): string | null {
1829 return null;
1830 }
1831
1856 -function diffHydratedStyles(domElement: Element, value: mixed) {
1832 +export function getPropsFromElement(domElement: Element): Object {
1833 + const serverDifferences: {[propName: string]: mixed} = {};
1834 + const attributes = domElement.attributes;
1835 + for (let i = 0; i < attributes.length; i++) {
1836 + const attr = attributes[i];
1837 + serverDifferences[attr.name] =
1838 + attr.name.toLowerCase() === 'style'
1839 + ? getStylesObjectFromElement(domElement)
1840 + : attr.value;
1841 + }
1842 + return serverDifferences;
1843 +}
1844 +
1845 +function getStylesObjectFromElement(domElement: Element): {
1846 + [styleName: string]: string,
1847 +} {
1848 + const serverValueInObjectForm: {[prop: string]: string} = {};
1849 + const style = ((domElement: any): HTMLElement).style;
1850 + for (let i = 0; i < style.length; i++) {
1851 + const styleName: string = style[i];
1852 + // TODO: We should use the original prop value here if it is equivalent.
1853 + // TODO: We could use the original client capitalization if the equivalent
1854 + // other capitalization exists in the DOM.
1855 + serverValueInObjectForm[styleName] = style.getPropertyValue(styleName);
1856 + }
1857 + return serverValueInObjectForm;
1858 +}
1859 +
1860 +function diffHydratedStyles(
1861 + domElement: Element,
1862 + value: mixed,
1863 + serverDifferences: {[propName: string]: mixed},
1864 +): void {
1865 if (value != null && typeof value !== 'object') {
1858 - throw new Error(
1859 - 'The `style` prop expects a mapping from style properties to values, ' +
1860 - "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
1861 - 'using JSX.',
1862 - );
1866 + if (__DEV__) {
1867 + console.error(
1868 + 'The `style` prop expects a mapping from style properties to values, ' +
1869 + "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
1870 + 'using JSX.',
1871 + );
1872 + }
1873 + return;
1874 }
1875 if (canDiffStyleForHydrationWarning) {
1865 - const expectedStyle = createDangerousStringForStyles(value);
1876 + // First we compare the string form and see if it's equivalent.
1877 + // This lets us bail out on anything that used to pass in this form.
1878 + // It also lets us compare anything that's not parsed by this browser.
1879 + const clientValue = createDangerousStringForStyles(value);
1880 const serverValue = domElement.getAttribute('style');
1867 - warnForPropDifference('style', serverValue, expectedStyle);
1881 +
1882 + if (serverValue === clientValue) {
1883 + return;
1884 + }
1885 + const normalizedClientValue =
1886 + normalizeMarkupForTextOrAttribute(clientValue);
1887 + const normalizedServerValue =
1888 + normalizeMarkupForTextOrAttribute(serverValue);
1889 + if (normalizedServerValue === normalizedClientValue) {
1890 + return;
1891 + }
1892 +
1893 + // Otherwise, we create the object from the DOM for the diff view.
1894 + serverDifferences.style = getStylesObjectFromElement(domElement);
1895 }
1896 }
1897
@@ -1874,6 +1901,7 @@ function hydrateAttribute(
1901 attributeName: string,
1902 value: any,
1903 extraAttributes: Set<string>,
1904 + serverDifferences: {[propName: string]: mixed},
1905 ): void {
1906 extraAttributes.delete(attributeName);
1907 const serverValue = domElement.getAttribute(attributeName);
@@ -1906,7 +1934,7 @@ function hydrateAttribute(
1934 }
1935 }
1936 }
1909 - warnForPropDifference(propKey, serverValue, value);
1937 + warnForPropDifference(propKey, serverValue, value, serverDifferences);
1938 }
1939
1940 function hydrateBooleanAttribute(
@@ -1915,6 +1943,7 @@ function hydrateBooleanAttribute(
1943 attributeName: string,
1944 value: any,
1945 extraAttributes: Set<string>,
1946 + serverDifferences: {[propName: string]: mixed},
1947 ): void {
1948 extraAttributes.delete(attributeName);
1949 const serverValue = domElement.getAttribute(attributeName);
@@ -1942,7 +1971,7 @@ function hydrateBooleanAttribute(
1971 }
1972 }
1973 }
1945 - warnForPropDifference(propKey, serverValue, value);
1974 + warnForPropDifference(propKey, serverValue, value, serverDifferences);
1975 }
1976
1977 function hydrateOverloadedBooleanAttribute(
@@ -1951,6 +1980,7 @@ function hydrateOverloadedBooleanAttribute(
1980 attributeName: string,
1981 value: any,
1982 extraAttributes: Set<string>,
1983 + serverDifferences: {[propName: string]: mixed},
1984 ): void {
1985 extraAttributes.delete(attributeName);
1986 const serverValue = domElement.getAttribute(attributeName);
@@ -1990,7 +2020,7 @@ function hydrateOverloadedBooleanAttribute(
2020 }
2021 }
2022 }
1993 - warnForPropDifference(propKey, serverValue, value);
2023 + warnForPropDifference(propKey, serverValue, value, serverDifferences);
2024 }
2025
2026 function hydrateBooleanishAttribute(
@@ -1999,6 +2029,7 @@ function hydrateBooleanishAttribute(
2029 attributeName: string,
2030 value: any,
2031 extraAttributes: Set<string>,
2032 + serverDifferences: {[propName: string]: mixed},
2033 ): void {
2034 extraAttributes.delete(attributeName);
2035 const serverValue = domElement.getAttribute(attributeName);
@@ -2029,7 +2060,7 @@ function hydrateBooleanishAttribute(
2060 }
2061 }
2062 }
2032 - warnForPropDifference(propKey, serverValue, value);
2063 + warnForPropDifference(propKey, serverValue, value, serverDifferences);
2064 }
2065
2066 function hydrateNumericAttribute(
@@ -2038,6 +2069,7 @@ function hydrateNumericAttribute(
2069 attributeName: string,
2070 value: any,
2071 extraAttributes: Set<string>,
2072 + serverDifferences: {[propName: string]: mixed},
2073 ): void {
2074 extraAttributes.delete(attributeName);
2075 const serverValue = domElement.getAttribute(attributeName);
@@ -2079,7 +2111,7 @@ function hydrateNumericAttribute(
2111 }
2112 }
2113 }
2082 - warnForPropDifference(propKey, serverValue, value);
2114 + warnForPropDifference(propKey, serverValue, value, serverDifferences);
2115 }
2116
2117 function hydratePositiveNumericAttribute(
@@ -2088,6 +2120,7 @@ function hydratePositiveNumericAttribute(
2120 attributeName: string,
2121 value: any,
2122 extraAttributes: Set<string>,
2123 + serverDifferences: {[propName: string]: mixed},
2124 ): void {
2125 extraAttributes.delete(attributeName);
2126 const serverValue = domElement.getAttribute(attributeName);
@@ -2129,7 +2162,7 @@ function hydratePositiveNumericAttribute(
2162 }
2163 }
2164 }
2132 - warnForPropDifference(propKey, serverValue, value);
2165 + warnForPropDifference(propKey, serverValue, value, serverDifferences);
2166 }
2167
2168 function hydrateSanitizedAttribute(
@@ -2138,6 +2171,7 @@ function hydrateSanitizedAttribute(
2171 attributeName: string,
2172 value: any,
2173 extraAttributes: Set<string>,
2174 + serverDifferences: {[propName: string]: mixed},
2175 ): void {
2176 extraAttributes.delete(attributeName);
2177 const serverValue = domElement.getAttribute(attributeName);
@@ -2171,7 +2205,7 @@ function hydrateSanitizedAttribute(
2205 }
2206 }
2207 }
2174 - warnForPropDifference(propKey, serverValue, value);
2208 + warnForPropDifference(propKey, serverValue, value, serverDifferences);
2209 }
2210
2211 function diffHydratedCustomComponent(
@@ -2180,6 +2214,7 @@ function diffHydratedCustomComponent(
2214 props: Object,
2215 hostContext: HostContext,
2216 extraAttributes: Set<string>,
2217 + serverDifferences: {[propName: string]: mixed},
2218 ) {
2219 for (const propKey in props) {
2220 if (!props.hasOwnProperty(propKey)) {
@@ -2201,7 +2236,18 @@ function diffHydratedCustomComponent(
2236 }
2237 // Validate that the properties correspond to their expected values.
2238 switch (propKey) {
2204 - case 'children': // Checked above already
2239 + case 'children': {
2240 + if (typeof value === 'string' || typeof value === 'number') {
2241 + warnForPropDifference(
2242 + 'children',
2243 + domElement.textContent,
2244 + value,
2245 + serverDifferences,
2246 + );
2247 + }
2248 + continue;
2249 + }
2250 + // Checked above already
2251 case 'suppressContentEditableWarning':
2252 case 'suppressHydrationWarning':
2253 case 'defaultValue':
@@ -2215,12 +2261,17 @@ function diffHydratedCustomComponent(
2261 const nextHtml = value ? value.__html : undefined;
2262 if (nextHtml != null) {
2263 const expectedHTML = normalizeHTML(domElement, nextHtml);
2218 - warnForPropDifference(propKey, serverHTML, expectedHTML);
2264 + warnForPropDifference(
2265 + propKey,
2266 + serverHTML,
2267 + expectedHTML,
2268 + serverDifferences,
2269 + );
2270 }
2271 continue;
2272 case 'style':
2273 extraAttributes.delete(propKey);
2223 - diffHydratedStyles(domElement, value);
2274 + diffHydratedStyles(domElement, value, serverDifferences);
2275 continue;
2276 case 'offsetParent':
2277 case 'offsetTop':
@@ -2250,7 +2301,12 @@ function diffHydratedCustomComponent(
2301 'class',
2302 value,
2303 );
2253 - warnForPropDifference('className', serverValue, value);
2304 + warnForPropDifference(
2305 + 'className',
2306 + serverValue,
2307 + value,
2308 + serverDifferences,
2309 + );
2310 continue;
2311 }
2312 // Fall through
@@ -2272,7 +2328,7 @@ function diffHydratedCustomComponent(
2328 propKey,
2329 value,
2330 );
2275 - warnForPropDifference(propKey, serverValue, value);
2331 + warnForPropDifference(propKey, serverValue, value, serverDifferences);
2332 }
2333 }
2334 }
@@ -2291,6 +2347,7 @@ function diffHydratedGenericElement(
2347 props: Object,
2348 hostContext: HostContext,
2349 extraAttributes: Set<string>,
2350 + serverDifferences: {[propName: string]: mixed},
2351 ) {
2352 for (const propKey in props) {
2353 if (!props.hasOwnProperty(propKey)) {
@@ -2312,7 +2369,18 @@ function diffHydratedGenericElement(
2369 }
2370 // Validate that the properties correspond to their expected values.
2371 switch (propKey) {
2315 - case 'children': // Checked above already
2372 + case 'children': {
2373 + if (typeof value === 'string' || typeof value === 'number') {
2374 + warnForPropDifference(
2375 + 'children',
2376 + domElement.textContent,
2377 + value,
2378 + serverDifferences,
2379 + );
2380 + }
2381 + continue;
2382 + }
2383 + // Checked above already
2384 case 'suppressContentEditableWarning':
2385 case 'suppressHydrationWarning':
2386 case 'value': // Controlled attributes are not validated
@@ -2329,11 +2397,22 @@ function diffHydratedGenericElement(
2397 const nextHtml = value ? value.__html : undefined;
2398 if (nextHtml != null) {
2399 const expectedHTML = normalizeHTML(domElement, nextHtml);
2332 - warnForPropDifference(propKey, serverHTML, expectedHTML);
2400 + if (serverHTML !== expectedHTML) {
2401 + serverDifferences[propKey] = {
2402 + __html: serverHTML,
2403 + };
2404 + }
2405 }
2406 continue;
2407 case 'className':
2336 - hydrateAttribute(domElement, propKey, 'class', value, extraAttributes);
2408 + hydrateAttribute(
2409 + domElement,
2410 + propKey,
2411 + 'class',
2412 + value,
2413 + extraAttributes,
2414 + serverDifferences,
2415 + );
2416 continue;
2417 case 'tabIndex':
2418 hydrateAttribute(
@@ -2342,28 +2421,29 @@ function diffHydratedGenericElement(
2421 'tabindex',
2422 value,
2423 extraAttributes,
2424 + serverDifferences,
2425 );
2426 continue;
2427 case 'style':
2428 extraAttributes.delete(propKey);
2349 - diffHydratedStyles(domElement, value);
2429 + diffHydratedStyles(domElement, value, serverDifferences);
2430 continue;
2431 case 'multiple': {
2432 extraAttributes.delete(propKey);
2433 const serverValue = (domElement: any).multiple;
2354 - warnForPropDifference(propKey, serverValue, value);
2434 + warnForPropDifference(propKey, serverValue, value, serverDifferences);
2435 continue;
2436 }
2437 case 'muted': {
2438 extraAttributes.delete(propKey);
2439 const serverValue = (domElement: any).muted;
2360 - warnForPropDifference(propKey, serverValue, value);
2440 + warnForPropDifference(propKey, serverValue, value, serverDifferences);
2441 continue;
2442 }
2443 case 'autoFocus': {
2444 extraAttributes.delete('autofocus');
2445 const serverValue = (domElement: any).autofocus;
2366 - warnForPropDifference(propKey, serverValue, value);
2446 + warnForPropDifference(propKey, serverValue, value, serverDifferences);
2447 continue;
2448 }
2449 case 'src':
@@ -2400,6 +2480,7 @@ function diffHydratedGenericElement(
2480 propKey,
2481 null,
2482 extraAttributes,
2483 + serverDifferences,
2484 );
2485 continue;
2486 }
@@ -2410,6 +2491,7 @@ function diffHydratedGenericElement(
2491 propKey,
2492 value,
2493 extraAttributes,
2494 + serverDifferences,
2495 );
2496 continue;
2497 case 'action':
@@ -2438,7 +2520,7 @@ function diffHydratedGenericElement(
2520 continue;
2521 } else if (serverValue === EXPECTED_FORM_ACTION_URL) {
2522 extraAttributes.delete(propKey.toLowerCase());
2441 - warnForPropDifference(propKey, 'function', value);
2523 + warnForPropDifference(propKey, 'function', value, serverDifferences);
2524 continue;
2525 }
2526 hydrateSanitizedAttribute(
@@ -2447,6 +2529,7 @@ function diffHydratedGenericElement(
2529 propKey.toLowerCase(),
2530 value,
2531 extraAttributes,
2532 + serverDifferences,
2533 );
2534 continue;
2535 }
@@ -2457,6 +2540,7 @@ function diffHydratedGenericElement(
2540 'xlink:href',
2541 value,
2542 extraAttributes,
2543 + serverDifferences,
2544 );
2545 continue;
2546 case 'contentEditable': {
@@ -2467,6 +2551,7 @@ function diffHydratedGenericElement(
2551 'contenteditable',
2552 value,
2553 extraAttributes,
2554 + serverDifferences,
2555 );
2556 continue;
2557 }
@@ -2478,6 +2563,7 @@ function diffHydratedGenericElement(
2563 'spellcheck',
2564 value,
2565 extraAttributes,
2566 + serverDifferences,
2567 );
2568 continue;
2569 }
@@ -2493,6 +2579,7 @@ function diffHydratedGenericElement(
2579 propKey,
2580 value,
2581 extraAttributes,
2582 + serverDifferences,
2583 );
2584 continue;
2585 }
@@ -2525,6 +2612,7 @@ function diffHydratedGenericElement(
2612 propKey.toLowerCase(),
2613 value,
2614 extraAttributes,
2615 + serverDifferences,
2616 );
2617 continue;
2618 }
@@ -2536,6 +2624,7 @@ function diffHydratedGenericElement(
2624 propKey,
2625 value,
2626 extraAttributes,
2627 + serverDifferences,
2628 );
2629 continue;
2630 }
@@ -2549,6 +2638,7 @@ function diffHydratedGenericElement(
2638 propKey,
2639 value,
2640 extraAttributes,
2641 + serverDifferences,
2642 );
2643 continue;
2644 }
@@ -2559,6 +2649,7 @@ function diffHydratedGenericElement(
2649 'rowspan',
2650 value,
2651 extraAttributes,
2652 + serverDifferences,
2653 );
2654 continue;
2655 }
@@ -2569,6 +2660,7 @@ function diffHydratedGenericElement(
2660 propKey,
2661 value,
2662 extraAttributes,
2663 + serverDifferences,
2664 );
2665 continue;
2666 }
@@ -2579,6 +2671,7 @@ function diffHydratedGenericElement(
2671 'x-height',
2672 value,
2673 extraAttributes,
2674 + serverDifferences,
2675 );
2676 continue;
2677 case 'xlinkActuate':
@@ -2588,6 +2681,7 @@ function diffHydratedGenericElement(
2681 'xlink:actuate',
2682 value,
2683 extraAttributes,
2684 + serverDifferences,
2685 );
2686 continue;
2687 case 'xlinkArcrole':
@@ -2597,6 +2691,7 @@ function diffHydratedGenericElement(
2691 'xlink:arcrole',
2692 value,
2693 extraAttributes,
2694 + serverDifferences,
2695 );
2696 continue;
2697 case 'xlinkRole':
@@ -2606,6 +2701,7 @@ function diffHydratedGenericElement(
2701 'xlink:role',
2702 value,
2703 extraAttributes,
2704 + serverDifferences,
2705 );
2706 continue;
2707 case 'xlinkShow':
@@ -2615,6 +2711,7 @@ function diffHydratedGenericElement(
2711 'xlink:show',
2712 value,
2713 extraAttributes,
2714 + serverDifferences,
2715 );
2716 continue;
2717 case 'xlinkTitle':
@@ -2624,6 +2721,7 @@ function diffHydratedGenericElement(
2721 'xlink:title',
2722 value,
2723 extraAttributes,
2724 + serverDifferences,
2725 );
2726 continue;
2727 case 'xlinkType':
@@ -2633,6 +2731,7 @@ function diffHydratedGenericElement(
2731 'xlink:type',
2732 value,
2733 extraAttributes,
2734 + serverDifferences,
2735 );
2736 continue;
2737 case 'xmlBase':
@@ -2642,6 +2741,7 @@ function diffHydratedGenericElement(
2741 'xml:base',
2742 value,
2743 extraAttributes,
2744 + serverDifferences,
2745 );
2746 continue;
2747 case 'xmlLang':
@@ -2651,6 +2751,7 @@ function diffHydratedGenericElement(
2751 'xml:lang',
2752 value,
2753 extraAttributes,
2754 + serverDifferences,
2755 );
2756 continue;
2757 case 'xmlSpace':
@@ -2660,6 +2761,7 @@ function diffHydratedGenericElement(
2761 'xml:space',
2762 value,
2763 extraAttributes,
2764 + serverDifferences,
2765 );
2766 continue;
2767 case 'inert':
@@ -2685,6 +2787,7 @@ function diffHydratedGenericElement(
2787 propKey,
2788 value,
2789 extraAttributes,
2790 + serverDifferences,
2791 );
2792 continue;
2793 }
@@ -2731,20 +2834,19 @@ function diffHydratedGenericElement(
2834 value,
2835 );
2836 if (!isMismatchDueToBadCasing) {
2734 - warnForPropDifference(propKey, serverValue, value);
2837 + warnForPropDifference(propKey, serverValue, value, serverDifferences);
2838 }
2839 }
2840 }
2841 }
2842 }
2843
2741 -export function diffHydratedProperties(
2844 +export function hydrateProperties(
2845 domElement: Element,
2846 tag: string,
2847 props: Object,
2745 - shouldWarnDev: boolean,
2848 hostContext: HostContext,
2747 -): void {
2849 +): boolean {
2850 if (__DEV__) {
2851 validatePropertiesInDevelopment(tag, props);
2852 }
@@ -2857,11 +2959,13 @@ export function diffHydratedProperties(
2959 typeof children === 'number' ||
2960 (enableBigIntSupport && typeof children === 'bigint')
2961 ) {
2860 - // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
2861 - if (domElement.textContent !== '' + children) {
2862 - if (props.suppressHydrationWarning !== true) {
2863 - checkForUnmatchedText(domElement.textContent, children, shouldWarnDev);
2864 - }
2962 + if (
2963 + // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
2964 + domElement.textContent !== '' + children &&
2965 + props.suppressHydrationWarning !== true &&
2966 + !checkForUnmatchedText(domElement.textContent, children)
2967 + ) {
2968 + return false;
2969 }
2970 }
2971
@@ -2878,7 +2982,17 @@ export function diffHydratedProperties(
2982 trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
2983 }
2984
2881 - if (__DEV__ && shouldWarnDev) {
2985 + return true;
2986 +}
2987 +
2988 +export function diffHydratedProperties(
2989 + domElement: Element,
2990 + tag: string,
2991 + props: Object,
2992 + hostContext: HostContext,
2993 +): null | Object {
2994 + const serverDifferences: {[propName: string]: mixed} = {};
2995 + if (__DEV__) {
2996 const extraAttributes: Set<string> = new Set();
2997 const attributes = domElement.attributes;
2998 for (let i = 0; i < attributes.length; i++) {
@@ -2905,6 +3019,7 @@ export function diffHydratedProperties(
3019 props,
3020 hostContext,
3021 extraAttributes,
3022 + serverDifferences,
3023 );
3024 } else {
3025 diffHydratedGenericElement(
@@ -2913,86 +3028,47 @@ export function diffHydratedProperties(
3028 props,
3029 hostContext,
3030 extraAttributes,
3031 + serverDifferences,
3032 );
3033 }
3034 if (extraAttributes.size > 0 && props.suppressHydrationWarning !== true) {
2919 - warnForExtraAttributes(extraAttributes);
3035 + warnForExtraAttributes(domElement, extraAttributes, serverDifferences);
3036 }
3037 }
2922 -}
2923 -
2924 -export function diffHydratedText(textNode: Text, text: string): boolean {
2925 - const isDifferent = textNode.nodeValue !== text;
2926 - return isDifferent;
2927 -}
2928 -
2929 -export function warnForDeletedHydratableElement(
2930 - parentNode: Element | Document | DocumentFragment,
2931 - child: Element,
2932 -) {
2933 - if (__DEV__) {
2934 - if (didWarnInvalidHydration) {
2935 - return;
2936 - }
2937 - didWarnInvalidHydration = true;
2938 - console.error(
2939 - 'Did not expect server HTML to contain a <%s> in <%s>.',
2940 - child.nodeName.toLowerCase(),
2941 - parentNode.nodeName.toLowerCase(),
2942 - );
3038 + if (Object.keys(serverDifferences).length === 0) {
3039 + return null;
3040 }
3041 + return serverDifferences;
3042 }
3043
2946 -export function warnForDeletedHydratableText(
2947 - parentNode: Element | Document | DocumentFragment,
2948 - child: Text,
2949 -) {
2950 - if (__DEV__) {
2951 - if (didWarnInvalidHydration) {
2952 - return;
2953 - }
2954 - didWarnInvalidHydration = true;
2955 - console.error(
2956 - 'Did not expect server HTML to contain the text node "%s" in <%s>.',
2957 - child.nodeValue,
2958 - parentNode.nodeName.toLowerCase(),
2959 - );
3044 +export function hydrateText(
3045 + textNode: Text,
3046 + text: string,
3047 + parentProps: null | Object,
3048 +): boolean {
3049 + const isDifferent = textNode.nodeValue !== text;
3050 + if (
3051 + isDifferent &&
3052 + (parentProps === null || parentProps.suppressHydrationWarning !== true) &&
3053 + !checkForUnmatchedText(textNode.nodeValue, text)
3054 + ) {
3055 + return false;
3056 }
3057 + return true;
3058 }
3059
2963 -export function warnForInsertedHydratedElement(
2964 - parentNode: Element | Document | DocumentFragment,
2965 - tag: string,
2966 - props: Object,
2967 -) {
2968 - if (__DEV__) {
2969 - if (didWarnInvalidHydration) {
2970 - return;
2971 - }
2972 - didWarnInvalidHydration = true;
2973 - console.error(
2974 - 'Expected server HTML to contain a matching <%s> in <%s>.',
2975 - tag,
2976 - parentNode.nodeName.toLowerCase(),
2977 - );
3060 +export function diffHydratedText(textNode: Text, text: string): null | string {
3061 + if (textNode.nodeValue === text) {
3062 + return null;
3063 }
2979 -}
2980 -
2981 -export function warnForInsertedHydratedText(
2982 - parentNode: Element | Document | DocumentFragment,
2983 - text: string,
2984 -) {
2985 - if (__DEV__) {
2986 - if (didWarnInvalidHydration) {
2987 - return;
2988 - }
2989 - didWarnInvalidHydration = true;
2990 - console.error(
2991 - 'Expected server HTML to contain a matching text node for "%s" in <%s>.',
2992 - text,
2993 - parentNode.nodeName.toLowerCase(),
2994 - );
3064 + const normalizedClientText = normalizeMarkupForTextOrAttribute(text);
3065 + const normalizedServerText = normalizeMarkupForTextOrAttribute(
3066 + textNode.nodeValue,
3067 + );
3068 + if (normalizedServerText === normalizedClientText) {
3069 + return null;
3070 }
3071 + return textNode.nodeValue;
3072 }
3073
3074 export function restoreControlledState(
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+52 -187
@@ -52,14 +52,12 @@ import {hasRole} from './DOMAccessibilityRoles';
52 import {
53 setInitialProperties,
54 updateProperties,
55 + hydrateProperties,
56 + hydrateText,
57 diffHydratedProperties,
58 + getPropsFromElement,
59 diffHydratedText,
60 trapClickOnNonInteractiveElement,
58 - checkForUnmatchedText,
59 - warnForDeletedHydratableElement,
60 - warnForDeletedHydratableText,
61 - warnForInsertedHydratedElement,
62 - warnForInsertedHydratedText,
61 } from './ReactDOMComponent';
62 import {getSelectionInformation, restoreSelection} from './ReactInputSelection';
63 import setTextContent from './setTextContent';
@@ -1342,6 +1340,26 @@ export function getFirstHydratableChildWithinSuspenseInstance(
1340 return getNextHydratable(parentInstance.nextSibling);
1341 }
1342
1343 +export function describeHydratableInstanceForDevWarnings(
1344 + instance: HydratableInstance,
1345 +): string | {type: string, props: $ReadOnly<Props>} {
1346 + // Reverse engineer a pseudo react-element from hydratable instnace
1347 + if (instance.nodeType === ELEMENT_NODE) {
1348 + // Reverse engineer a set of props that can print for dev warnings
1349 + return {
1350 + type: instance.nodeName.toLowerCase(),
1351 + props: getPropsFromElement((instance: any)),
1352 + };
1353 + } else if (instance.nodeType === COMMENT_NODE) {
1354 + return {
1355 + type: 'Suspense',
1356 + props: {},
1357 + };
1358 + } else {
1359 + return instance.nodeValue;
1360 + }
1361 +}
1362 +
1363 export function validateHydratableInstance(
1364 type: string,
1365 props: Props,
@@ -1361,14 +1379,23 @@ export function hydrateInstance(
1379 props: Props,
1380 hostContext: HostContext,
1381 internalInstanceHandle: Object,
1364 - shouldWarnDev: boolean,
1365 -): void {
1382 +): boolean {
1383 precacheFiberNode(internalInstanceHandle, instance);
1384 // TODO: Possibly defer this until the commit phase where all the events
1385 // get attached.
1386 updateFiberProps(instance, props);
1387
1371 - diffHydratedProperties(instance, type, props, shouldWarnDev, hostContext);
1388 + return hydrateProperties(instance, type, props, hostContext);
1389 +}
1390 +
1391 +// Returns a Map of properties that were different on the server.
1392 +export function diffHydratedPropsForDevWarnings(
1393 + instance: Instance,
1394 + type: string,
1395 + props: Props,
1396 + hostContext: HostContext,
1397 +): null | $ReadOnly<Props> {
1398 + return diffHydratedProperties(instance, type, props, hostContext);
1399 }
1400
1401 export function validateHydratableTextInstance(
@@ -1389,11 +1416,26 @@ export function hydrateTextInstance(
1416 textInstance: TextInstance,
1417 text: string,
1418 internalInstanceHandle: Object,
1392 - shouldWarnDev: boolean,
1419 + parentInstanceProps: null | Props,
1420 ): boolean {
1421 precacheFiberNode(internalInstanceHandle, textInstance);
1422
1396 - return diffHydratedText(textInstance, text);
1423 + return hydrateText(textInstance, text, parentInstanceProps);
1424 +}
1425 +
1426 +// Returns the server text if it differs from the client.
1427 +export function diffHydratedTextForDevWarnings(
1428 + textInstance: TextInstance,
1429 + text: string,
1430 + parentProps: null | Props,
1431 +): null | string {
1432 + if (
1433 + parentProps === null ||
1434 + parentProps[SUPPRESS_HYDRATION_WARNING] !== true
1435 + ) {
1436 + return diffHydratedText(textInstance, text);
1437 + }
1438 + return null;
1439 }
1440
1441 export function hydrateSuspenseInstance(
@@ -1485,183 +1527,6 @@ export function shouldDeleteUnhydratedTailInstances(
1527 return parentType !== 'form' && parentType !== 'button';
1528 }
1529
1488 -export function didNotMatchHydratedContainerTextInstance(
1489 - parentContainer: Container,
1490 - textInstance: TextInstance,
1491 - text: string,
1492 - shouldWarnDev: boolean,
1493 -) {
1494 - checkForUnmatchedText(textInstance.nodeValue, text, shouldWarnDev);
1495 -}
1496 -
1497 -export function didNotMatchHydratedTextInstance(
1498 - parentType: string,
1499 - parentProps: Props,
1500 - parentInstance: Instance,
1501 - textInstance: TextInstance,
1502 - text: string,
1503 - shouldWarnDev: boolean,
1504 -) {
1505 - if (parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
1506 - checkForUnmatchedText(textInstance.nodeValue, text, shouldWarnDev);
1507 - }
1508 -}
1509 -
1510 -export function didNotHydrateInstanceWithinContainer(
1511 - parentContainer: Container,
1512 - instance: HydratableInstance,
1513 -) {
1514 - if (__DEV__) {
1515 - if (instance.nodeType === ELEMENT_NODE) {
1516 - warnForDeletedHydratableElement(parentContainer, (instance: any));
1517 - } else if (instance.nodeType === COMMENT_NODE) {
1518 - // TODO: warnForDeletedHydratableSuspenseBoundary
1519 - } else {
1520 - warnForDeletedHydratableText(parentContainer, (instance: any));
1521 - }
1522 - }
1523 -}
1524 -
1525 -export function didNotHydrateInstanceWithinSuspenseInstance(
1526 - parentInstance: SuspenseInstance,
1527 - instance: HydratableInstance,
1528 -) {
1529 - if (__DEV__) {
1530 - // $FlowFixMe[incompatible-type]: Only Element or Document can be parent nodes.
1531 - const parentNode: Element | Document | null = parentInstance.parentNode;
1532 - if (parentNode !== null) {
1533 - if (instance.nodeType === ELEMENT_NODE) {
1534 - warnForDeletedHydratableElement(parentNode, (instance: any));
1535 - } else if (instance.nodeType === COMMENT_NODE) {
1536 - // TODO: warnForDeletedHydratableSuspenseBoundary
1537 - } else {
1538 - warnForDeletedHydratableText(parentNode, (instance: any));
1539 - }
1540 - }
1541 - }
1542 -}
1543 -
1544 -export function didNotHydrateInstance(
1545 - parentType: string,
1546 - parentProps: Props,
1547 - parentInstance: Instance,
1548 - instance: HydratableInstance,
1549 -) {
1550 - if (__DEV__) {
1551 - if (instance.nodeType === ELEMENT_NODE) {
1552 - warnForDeletedHydratableElement(parentInstance, (instance: any));
1553 - } else if (instance.nodeType === COMMENT_NODE) {
1554 - // TODO: warnForDeletedHydratableSuspenseBoundary
1555 - } else {
1556 - warnForDeletedHydratableText(parentInstance, (instance: any));
1557 - }
1558 - }
1559 -}
1560 -
1561 -export function didNotFindHydratableInstanceWithinContainer(
1562 - parentContainer: Container,
1563 - type: string,
1564 - props: Props,
1565 -) {
1566 - if (__DEV__) {
1567 - warnForInsertedHydratedElement(parentContainer, type, props);
1568 - }
1569 -}
1570 -
1571 -export function didNotFindHydratableTextInstanceWithinContainer(
1572 - parentContainer: Container,
1573 - text: string,
1574 -) {
1575 - if (__DEV__) {
1576 - warnForInsertedHydratedText(parentContainer, text);
1577 - }
1578 -}
1579 -
1580 -export function didNotFindHydratableSuspenseInstanceWithinContainer(
1581 - parentContainer: Container,
1582 -) {
1583 - if (__DEV__) {
1584 - // TODO: warnForInsertedHydratedSuspense(parentContainer);
1585 - }
1586 -}
1587 -
1588 -export function didNotFindHydratableInstanceWithinSuspenseInstance(
1589 - parentInstance: SuspenseInstance,
1590 - type: string,
1591 - props: Props,
1592 -) {
1593 - if (__DEV__) {
1594 - // $FlowFixMe[incompatible-type]: Only Element or Document can be parent nodes.
1595 - const parentNode: Element | Document | null = parentInstance.parentNode;
1596 - if (parentNode !== null)
1597 - warnForInsertedHydratedElement(parentNode, type, props);
1598 - }
1599 -}
1600 -
1601 -export function didNotFindHydratableTextInstanceWithinSuspenseInstance(
1602 - parentInstance: SuspenseInstance,
1603 - text: string,
1604 -) {
1605 - if (__DEV__) {
1606 - // $FlowFixMe[incompatible-type]: Only Element or Document can be parent nodes.
1607 - const parentNode: Element | Document | null = parentInstance.parentNode;
1608 - if (parentNode !== null) warnForInsertedHydratedText(parentNode, text);
1609 - }
1610 -}
1611 -
1612 -export function didNotFindHydratableSuspenseInstanceWithinSuspenseInstance(
1613 - parentInstance: SuspenseInstance,
1614 -) {
1615 - if (__DEV__) {
1616 - // const parentNode: Element | Document | null = parentInstance.parentNode;
1617 - // TODO: warnForInsertedHydratedSuspense(parentNode);
1618 - }
1619 -}
1620 -
1621 -export function didNotFindHydratableInstance(
1622 - parentType: string,
1623 - parentProps: Props,
1624 - parentInstance: Instance,
1625 - type: string,
1626 - props: Props,
1627 -) {
1628 - if (__DEV__) {
1629 - warnForInsertedHydratedElement(parentInstance, type, props);
1630 - }
1631 -}
1632 -
1633 -export function didNotFindHydratableTextInstance(
1634 - parentType: string,
1635 - parentProps: Props,
1636 - parentInstance: Instance,
1637 - text: string,
1638 -) {
1639 - if (__DEV__) {
1640 - warnForInsertedHydratedText(parentInstance, text);
1641 - }
1642 -}
1643 -
1644 -export function didNotFindHydratableSuspenseInstance(
1645 - parentType: string,
1646 - parentProps: Props,
1647 - parentInstance: Instance,
1648 -) {
1649 - if (__DEV__) {
1650 - // TODO: warnForInsertedHydratedSuspense(parentInstance);
1651 - }
1652 -}
1653 -
1654 -export function errorHydratingContainer(parentContainer: Container): void {
1655 - if (__DEV__) {
1656 - // TODO: This gets logged by onRecoverableError, too, so we should be
1657 - // able to remove it.
1658 - console.error(
1659 - 'An error occurred during hydration. The server HTML was replaced with client content in <%s>.',
1660 - parentContainer.nodeName.toLowerCase(),
1661 - );
1662 - }
1663 -}
1664 -
1530 // -------------------
1531 // Test Selectors
1532 // -------------------
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+4 -4
@@ -2407,8 +2407,8 @@ describe('ReactDOMFizzServer', () => {
2407 ]);
2408 }).toErrorDev(
2409 [
2410 - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.',
2411 - 'Warning: Expected server HTML to contain a matching <div> in <div>.\n' +
2410 + 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
2411 + 'Warning: Expected server HTML to contain a matching <div> in the root.\n' +
2412 ' in div (at **)\n' +
2413 ' in App (at **)',
2414 ],
@@ -2492,7 +2492,7 @@ describe('ReactDOMFizzServer', () => {
2492 }).toErrorDev(
2493 [
2494 'Warning: An error occurred during hydration. The server HTML was replaced with client content',
2495 - 'Warning: Expected server HTML to contain a matching <div> in <div>.\n' +
2495 + 'Warning: Expected server HTML to contain a matching <div> in the root.\n' +
2496 ' in div (at **)\n' +
2497 ' in App (at **)',
2498 ],
@@ -6343,7 +6343,7 @@ describe('ReactDOMFizzServer', () => {
6343 await waitForAll([]);
6344 }).toErrorDev(
6345 [
6346 - 'Expected server HTML to contain a matching <span> in <div>',
6346 + 'Expected server HTML to contain a matching <span> in the root',
6347 'An error occurred during hydration',
6348 ],
6349 {withoutStack: 1},
packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js
+7 -7
@@ -248,7 +248,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
248 }).toErrorDev(
249 [
250 'Expected server HTML to contain a matching <span> in <span>',
251 - 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
251 + 'An error occurred during hydration. The server HTML was replaced with client content.',
252 ],
253 {withoutStack: 1},
254 );
@@ -337,7 +337,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
337 }).toErrorDev(
338 [
339 'Did not expect server HTML to contain the text node "Server" in <span>',
340 - 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
340 + 'An error occurred during hydration. The server HTML was replaced with client content.',
341 ],
342 {withoutStack: 1},
343 );
@@ -385,7 +385,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
385 }).toErrorDev(
386 [
387 'Expected server HTML to contain a matching text node for "Client" in <span>.',
388 - 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
388 + 'An error occurred during hydration. The server HTML was replaced with client content.',
389 ],
390 {withoutStack: 1},
391 );
@@ -436,7 +436,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
436 }).toErrorDev(
437 [
438 'Did not expect server HTML to contain the text node "Server" in <span>.',
439 - 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
439 + 'An error occurred during hydration. The server HTML was replaced with client content.',
440 ],
441 {withoutStack: 1},
442 );
@@ -485,7 +485,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
485 }).toErrorDev(
486 [
487 'Expected server HTML to contain a matching text node for "Client" in <span>.',
488 - 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
488 + 'An error occurred during hydration. The server HTML was replaced with client content.',
489 ],
490 {withoutStack: 1},
491 );
@@ -608,7 +608,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
608 }).toErrorDev(
609 [
610 'Expected server HTML to contain a matching <p> in <div>.',
611 - 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
611 + 'An error occurred during hydration. The server HTML was replaced with client content.',
612 ],
613 {withoutStack: 1},
614 );
@@ -654,7 +654,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => {
654 }).toErrorDev(
655 [
656 'Did not expect server HTML to contain a <p> in <div>.',
657 - 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
657 + 'An error occurred during hydration. The server HTML was replaced with client content.',
658 ],
659 {withoutStack: 1},
660 );
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+2 -2
@@ -6481,7 +6481,7 @@ body {
6481 }).toErrorDev(
6482 [
6483 'Warning: Text content did not match. Server: "server" Client: "client"',
6484 - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
6484 + 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
6485 ],
6486 {withoutStack: 1},
6487 );
@@ -8271,7 +8271,7 @@ background-color: green;
8271 }).toErrorDev(
8272 [
8273 'Warning: Text content did not match. Server: "server" Client: "client"',
8274 - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
8274 + 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
8275 ],
8276 {withoutStack: 1},
8277 );
packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
+43 -36
@@ -86,7 +86,7 @@ describe('ReactDOMServerHydration', () => {
86 in main (at **)
87 in div (at **)
88 in Mismatch (at **)",
89 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
89 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
90 "Caught [Text content does not match server-rendered HTML.]",
91 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
92 ]
@@ -112,7 +112,7 @@ describe('ReactDOMServerHydration', () => {
112 "Warning: Text content did not match. Server: "This markup contains an nbsp entity:   server text" Client: "This markup contains an nbsp entity:   client text"
113 in div (at **)
114 in Mismatch (at **)",
115 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
115 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
116 "Caught [Text content does not match server-rendered HTML.]",
117 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
118 ]
@@ -138,7 +138,7 @@ describe('ReactDOMServerHydration', () => {
138 }
139 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
140 [
141 - "Warning: Prop \`dangerouslySetInnerHTML\` did not match. Server: "<span>server</span>" Client: "<span>client</span>"
141 + "Warning: Prop \`dangerouslySetInnerHTML\` did not match. Server: {"__html":"<span>server</span>"} Client: {"__html":"<span>client</span>"}
142 in main (at **)
143 in div (at **)
144 in Mismatch (at **)",
@@ -185,7 +185,7 @@ describe('ReactDOMServerHydration', () => {
185 }
186 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
187 [
188 - "Warning: Prop \`tabIndex\` did not match. Server: "null" Client: "1"
188 + "Warning: Prop \`tabIndex\` did not match. Server: null Client: 1
189 in main (at **)
190 in div (at **)
191 in Mismatch (at **)",
@@ -208,7 +208,7 @@ describe('ReactDOMServerHydration', () => {
208 }
209 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
210 [
211 - "Warning: Extra attributes from the server: tabindex,dir
211 + "Warning: Extra attribute from the server: tabindex
212 in main (at **)
213 in div (at **)
214 in Mismatch (at **)",
@@ -231,7 +231,7 @@ describe('ReactDOMServerHydration', () => {
231 }
232 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
233 [
234 - "Warning: Prop \`tabIndex\` did not match. Server: "null" Client: "1"
234 + "Warning: Prop \`tabIndex\` did not match. Server: null Client: 1
235 in main (at **)
236 in div (at **)
237 in Mismatch (at **)",
@@ -255,7 +255,7 @@ describe('ReactDOMServerHydration', () => {
255 }
256 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
257 [
258 - "Warning: Prop \`style\` did not match. Server: "opacity:0" Client: "opacity:1"
258 + "Warning: Prop \`style\` did not match. Server: {"opacity":"0"} Client: {"opacity":1}
259 in main (at **)
260 in div (at **)
261 in Mismatch (at **)",
@@ -281,7 +281,7 @@ describe('ReactDOMServerHydration', () => {
281 in main (at **)
282 in div (at **)
283 in Mismatch (at **)",
284 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
284 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
285 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
286 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
287 ]
@@ -305,7 +305,7 @@ describe('ReactDOMServerHydration', () => {
305 in header (at **)
306 in div (at **)
307 in Mismatch (at **)",
308 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
308 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
309 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
310 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
311 ]
@@ -329,7 +329,7 @@ describe('ReactDOMServerHydration', () => {
329 in main (at **)
330 in div (at **)
331 in Mismatch (at **)",
332 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
332 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
333 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
334 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
335 ]
@@ -353,7 +353,7 @@ describe('ReactDOMServerHydration', () => {
353 in footer (at **)
354 in div (at **)
355 in Mismatch (at **)",
356 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
356 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
357 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
358 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
359 ]
@@ -372,7 +372,7 @@ describe('ReactDOMServerHydration', () => {
372 "Warning: Text content did not match. Server: "" Client: "only"
373 in div (at **)
374 in Mismatch (at **)",
375 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
375 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
376 "Caught [Text content does not match server-rendered HTML.]",
377 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
378 ]
@@ -395,7 +395,7 @@ describe('ReactDOMServerHydration', () => {
395 "Warning: Expected server HTML to contain a matching text node for "second" in <div>.
396 in div (at **)
397 in Mismatch (at **)",
398 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
398 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
399 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
400 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
401 ]
@@ -418,7 +418,7 @@ describe('ReactDOMServerHydration', () => {
418 "Warning: Expected server HTML to contain a matching text node for "first" in <div>.
419 in div (at **)
420 in Mismatch (at **)",
421 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
421 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
422 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
423 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
424 ]
@@ -441,7 +441,7 @@ describe('ReactDOMServerHydration', () => {
441 "Warning: Expected server HTML to contain a matching text node for "third" in <div>.
442 in div (at **)
443 in Mismatch (at **)",
444 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
444 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
445 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
446 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
447 ]
@@ -466,7 +466,7 @@ describe('ReactDOMServerHydration', () => {
466 "Warning: Did not expect server HTML to contain a <main> in <div>.
467 in div (at **)
468 in Mismatch (at **)",
469 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
469 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
470 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
471 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
472 ]
@@ -490,7 +490,7 @@ describe('ReactDOMServerHydration', () => {
490 in main (at **)
491 in div (at **)
492 in Mismatch (at **)",
493 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
493 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
494 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
495 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
496 ]
@@ -514,7 +514,7 @@ describe('ReactDOMServerHydration', () => {
514 in footer (at **)
515 in div (at **)
516 in Mismatch (at **)",
517 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
517 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
518 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
519 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
520 ]
@@ -537,7 +537,7 @@ describe('ReactDOMServerHydration', () => {
537 "Warning: Did not expect server HTML to contain a <footer> in <div>.
538 in div (at **)
539 in Mismatch (at **)",
540 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
540 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
541 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
542 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
543 ]
@@ -556,7 +556,7 @@ describe('ReactDOMServerHydration', () => {
556 "Warning: Did not expect server HTML to contain the text node "only" in <div>.
557 in div (at **)
558 in Mismatch (at **)",
559 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
559 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
560 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
561 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
562 ]
@@ -580,7 +580,7 @@ describe('ReactDOMServerHydration', () => {
580 in main (at **)
581 in div (at **)
582 in Mismatch (at **)",
583 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
583 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
584 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
585 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
586 ]
@@ -604,7 +604,7 @@ describe('ReactDOMServerHydration', () => {
604 in footer (at **)
605 in div (at **)
606 in Mismatch (at **)",
607 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
607 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
608 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
609 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
610 ]
@@ -627,7 +627,7 @@ describe('ReactDOMServerHydration', () => {
627 "Warning: Did not expect server HTML to contain the text node "third" in <div>.
628 in div (at **)
629 in Mismatch (at **)",
630 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
630 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
631 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
632 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
633 ]
@@ -655,10 +655,13 @@ describe('ReactDOMServerHydration', () => {
655 </div>
656 );
657 }
658 - // TODO: This message doesn't seem to have any useful details.
658 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
659 [
661 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
660 + "Warning: Expected server HTML to contain a matching <Suspense> in <div>.
661 + in Suspense (at **)
662 + in div (at **)
663 + in Mismatch (at **)",
664 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
665 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
666 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
667 ]
@@ -680,10 +683,10 @@ describe('ReactDOMServerHydration', () => {
683 }
684 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
685 [
683 - "Warning: Did not expect server HTML to contain a <main> in <div>.
686 + "Warning: Did not expect server HTML to contain a <Suspense> in <div>.
687 in div (at **)
688 in Mismatch (at **)",
686 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
689 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
690 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
691 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
692 ]
@@ -707,7 +710,11 @@ describe('ReactDOMServerHydration', () => {
710 // TODO: This message doesn't seem to have any useful details.
711 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
712 [
710 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
713 + "Warning: Expected server HTML to contain a matching <Suspense> in <div>.
714 + in Suspense (at **)
715 + in div (at **)
716 + in Mismatch (at **)",
717 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
718 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
719 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
720 ]
@@ -735,10 +742,10 @@ describe('ReactDOMServerHydration', () => {
742 // rendered suspense boundaries this test will likely change again
743 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
744 [
738 - "Warning: Did not expect server HTML to contain a <template> in <div>.
745 + "Warning: Did not expect server HTML to contain a <Suspense> in <div>.
746 in div (at **)
747 in Mismatch (at **)",
741 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
748 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
749 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
750 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
751 ]
@@ -760,7 +767,7 @@ describe('ReactDOMServerHydration', () => {
767 }
768 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
769 [
763 - "Warning: Expected server HTML to contain a matching <main> in <div>.
770 + "Warning: Expected server HTML to contain a matching <main> in <Suspense>.
771 in main (at **)
772 in Suspense (at **)
773 in div (at **)
@@ -786,7 +793,7 @@ describe('ReactDOMServerHydration', () => {
793 }
794 expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
795 [
789 - "Warning: Expected server HTML to contain a matching <footer> in <div>.
796 + "Warning: Expected server HTML to contain a matching <footer> in <Suspense>.
797 in footer (at **)
798 in Suspense (at **)
799 in div (at **)
@@ -872,7 +879,7 @@ describe('ReactDOMServerHydration', () => {
879 in header (at **)
880 in div (at **)
881 in Mismatch (at **)",
875 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
882 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
883 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
884 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
885 ]
@@ -899,7 +906,7 @@ describe('ReactDOMServerHydration', () => {
906 "Warning: Did not expect server HTML to contain a <header> in <div>.
907 in div (at **)
908 in Mismatch (at **)",
902 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
909 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
910 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
911 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
912 ]
@@ -951,7 +958,7 @@ describe('ReactDOMServerHydration', () => {
958 in div (at **)
959 in ProfileSettings (at **)
960 in Mismatch (at **)",
954 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
961 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
962 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
963 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
964 ]
@@ -998,7 +1005,7 @@ describe('ReactDOMServerHydration', () => {
1005 in div (at **)
1006 in ProfileSettings (at **)
1007 in Mismatch (at **)",
1001 - "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
1008 + "Warning: An error occurred during hydration. The server HTML was replaced with client content.",
1009 "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
1010 "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1011 ]
packages/react-dom/src/__tests__/ReactDOMOption-test.js
+1 -1
@@ -269,7 +269,7 @@ describe('ReactDOMOption', () => {
269 }).toErrorDev(
270 [
271 'Warning: Text content did not match. Server: "FooBaz" Client: "Foo"',
272 - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>',
272 + 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
273 'Warning: In HTML, <div> cannot be a child of <option>',
274 ],
275 {withoutStack: 1},
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+1 -1
@@ -171,7 +171,7 @@ describe('ReactDOMRoot', () => {
171 </div>,
172 );
173 await expect(async () => await waitForAll([])).toErrorDev(
174 - 'Extra attributes',
174 + 'Extra attribute',
175 );
176 });
177
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+13 -11
@@ -353,7 +353,7 @@ describe('ReactDOMServerPartialHydration', () => {
353 expect(lastCall).toEqual([
354 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
355 'article',
356 - 'section',
356 + 'Suspense',
357 '\n' +
358 ' in article (at **)\n' +
359 ' in Component (at **)\n' +
@@ -457,7 +457,7 @@ describe('ReactDOMServerPartialHydration', () => {
457 [
458 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
459 'article',
460 - 'section',
460 + 'Suspense',
461 '\n' +
462 ' in article (at **)\n' +
463 ' in Component (at **)\n' +
@@ -905,7 +905,7 @@ describe('ReactDOMServerPartialHydration', () => {
905 [
906 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
907 'article',
908 - 'section',
908 + 'Suspense',
909 '\n' +
910 ' in article (at **)\n' +
911 ' in Component (at **)\n' +
@@ -1011,7 +1011,7 @@ describe('ReactDOMServerPartialHydration', () => {
1011 [
1012 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
1013 'article',
1014 - 'section',
1014 + 'Suspense',
1015 '\n' +
1016 ' in article (at **)\n' +
1017 ' in Component (at **)\n' +
@@ -1121,7 +1121,7 @@ describe('ReactDOMServerPartialHydration', () => {
1121 [
1122 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
1123 'article',
1124 - 'section',
1124 + 'Suspense',
1125 '\n' +
1126 ' in article (at **)\n' +
1127 ' in Component (at **)\n' +
@@ -1298,7 +1298,9 @@ describe('ReactDOMServerPartialHydration', () => {
1298 },
1299 });
1300 });
1301 - }).toErrorDev('Did not expect server HTML to contain a <span> in <div>');
1301 + }).toErrorDev(
1302 + 'Did not expect server HTML to contain a <span> in <Suspense>',
1303 + );
1304
1305 expect(container.innerHTML).toContain('<span>A</span>');
1306 expect(container.innerHTML).not.toContain('<span>B</span>');
@@ -1376,7 +1378,7 @@ describe('ReactDOMServerPartialHydration', () => {
1378 expect(mockError).toHaveBeenCalledWith(
1379 'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s',
1380 'span',
1379 - 'div',
1381 + 'Suspense',
1382 '\n' +
1383 ' in Suspense (at **)\n' +
1384 ' in div (at **)\n' +
@@ -4032,8 +4034,8 @@ describe('ReactDOMServerPartialHydration', () => {
4034 }).toErrorDev(
4035 [
4036 'Warning: An error occurred during hydration. ' +
4035 - 'The server HTML was replaced with client content in <div>.',
4036 - 'Warning: Expected server HTML to contain a matching <span> in <div>.\n' +
4037 + 'The server HTML was replaced with client content.',
4038 + 'Warning: Expected server HTML to contain a matching <span> in the root.\n' +
4039 ' in span (at **)\n' +
4040 ' in App (at **)',
4041 ],
@@ -4079,7 +4081,7 @@ describe('ReactDOMServerPartialHydration', () => {
4081 [
4082 'Text content did not match. Server: "good" Client: "bad"',
4083 'An error occurred during hydration. The server HTML was replaced with ' +
4082 - 'client content in <div>.',
4084 + 'client content.',
4085 ],
4086 {withoutStack: 1},
4087 );
@@ -4123,7 +4125,7 @@ describe('ReactDOMServerPartialHydration', () => {
4125 [
4126 'Text content did not match. Server: "good" Client: "bad"',
4127 'An error occurred during hydration. The server HTML was replaced with ' +
4126 - 'client content in <div>.',
4128 + 'client content.',
4129 ],
4130 {withoutStack: 1},
4131 );
packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js
+1 -1
@@ -470,7 +470,7 @@ describe('ReactDOM HostSingleton', () => {
470 in div (at **)
471 in body (at **)
472 in html (at **)`,
473 - `Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.`,
473 + `Warning: An error occurred during hydration. The server HTML was replaced with client content.`,
474 ],
475 {withoutStack: 1},
476 );
packages/react-dom/src/__tests__/ReactRenderDocument-test.js
+5 -5
@@ -197,8 +197,8 @@ describe('rendering React components at document', () => {
197 });
198 }).toErrorDev(
199 [
200 - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.',
201 - 'Expected server HTML to contain a matching <div> in <div>.',
200 + 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
201 + 'Expected server HTML to contain a matching <div> in the root.',
202 ],
203 {withoutStack: 1},
204 );
@@ -233,7 +233,7 @@ describe('rendering React components at document', () => {
233 });
234 }).toErrorDev(
235 [
236 - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.',
236 + 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
237 'Expected server HTML to contain a matching <div> in <div>.',
238 ],
239 {withoutStack: 1},
@@ -279,7 +279,7 @@ describe('rendering React components at document', () => {
279 });
280 }).toErrorDev(
281 [
282 - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
282 + 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
283 'Warning: Text content did not match.',
284 ],
285 {
@@ -325,7 +325,7 @@ describe('rendering React components at document', () => {
325 });
326 }).toErrorDev(
327 [
328 - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
328 + 'Warning: An error occurred during hydration. The server HTML was replaced with client content.',
329 'Expected server HTML to contain a matching text node for "Hello world" in <body>',
330 ],
331 {withoutStack: 1},
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+10 -8
@@ -140,7 +140,7 @@ describe('ReactDOMServerHydration', () => {
140 });
141 }).toErrorDev(
142 [
143 - 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
143 + 'An error occurred during hydration. The server HTML was replaced with client content.',
144 'Text content did not match. Server: "x" Client: "y"',
145 ],
146 {withoutStack: 1},
@@ -225,7 +225,7 @@ describe('ReactDOMServerHydration', () => {
225 });
226 }).toErrorDev(
227 [
228 - 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
228 + 'An error occurred during hydration. The server HTML was replaced with client content.',
229 'Warning: Text content did not match. Server: "server" Client: "client"',
230 ],
231 {withoutStack: 1},
@@ -254,8 +254,9 @@ describe('ReactDOMServerHydration', () => {
254 });
255 }).toErrorDev(
256 'Warning: Prop `style` did not match. Server: ' +
257 - '"text-decoration:none;color:black;height:10px" Client: ' +
258 - '"text-decoration:none;color:white;height:10px"',
257 + '{"text-decoration":"none","color":"black","height":"10px"}' +
258 + ' Client: ' +
259 + '{"textDecoration":"none","color":"white","height":"10px"}',
260 );
261 });
262
@@ -303,8 +304,9 @@ describe('ReactDOMServerHydration', () => {
304 });
305 }).toErrorDev(
306 'Warning: Prop `style` did not match. Server: ' +
306 - '"text-decoration: none; color: black; height: 10px;" Client: ' +
307 - '"text-decoration:none;color:black;height:10px"',
307 + '{"text-decoration":"none","color":"black","height":"10px"}' +
308 + ' Client: ' +
309 + '{"textDecoration":"none","color":"black","height":"10px"}', // note that this is no difference
310 );
311 });
312
@@ -532,7 +534,7 @@ describe('ReactDOMServerHydration', () => {
534 expect(domElement.innerHTML).not.toEqual(markup);
535 }).toErrorDev(
536 [
535 - 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
537 + 'An error occurred during hydration. The server HTML was replaced with client content.',
538 'Warning: Text content did not match. Server: "server" Client: "client"',
539 ],
540 {withoutStack: 1},
@@ -558,7 +560,7 @@ describe('ReactDOMServerHydration', () => {
560 expect(domElement.innerHTML).not.toEqual(markup);
561 }).toErrorDev(
562 [
561 - 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
563 + 'An error occurred during hydration. The server HTML was replaced with client content.',
564 'Warning: Did not expect server HTML to contain a <p> in <div>.',
565 ],
566 {withoutStack: 1},
packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js
+3 -15
@@ -43,20 +43,8 @@ export const commitHydratedSuspenseInstance = shim;
43 export const clearSuspenseBoundary = shim;
44 export const clearSuspenseBoundaryFromContainer = shim;
45 export const shouldDeleteUnhydratedTailInstances = shim;
46 -export const didNotMatchHydratedContainerTextInstance = shim;
47 -export const didNotMatchHydratedTextInstance = shim;
48 -export const didNotHydrateInstanceWithinContainer = shim;
49 -export const didNotHydrateInstanceWithinSuspenseInstance = shim;
50 -export const didNotHydrateInstance = shim;
51 -export const didNotFindHydratableInstanceWithinContainer = shim;
52 -export const didNotFindHydratableTextInstanceWithinContainer = shim;
53 -export const didNotFindHydratableSuspenseInstanceWithinContainer = shim;
54 -export const didNotFindHydratableInstanceWithinSuspenseInstance = shim;
55 -export const didNotFindHydratableTextInstanceWithinSuspenseInstance = shim;
56 -export const didNotFindHydratableSuspenseInstanceWithinSuspenseInstance = shim;
57 -export const didNotFindHydratableInstance = shim;
58 -export const didNotFindHydratableTextInstance = shim;
59 -export const didNotFindHydratableSuspenseInstance = shim;
60 -export const errorHydratingContainer = shim;
46 +export const diffHydratedPropsForDevWarnings = shim;
47 +export const diffHydratedTextForDevWarnings = shim;
48 +export const describeHydratableInstanceForDevWarnings = shim;
49 export const validateHydratableInstance = shim;
50 export const validateHydratableTextInstance = shim;
packages/react-reconciler/src/ReactFiberHydrationContext.js
+229 -125
@@ -38,24 +38,13 @@ import {
38 getFirstHydratableChildWithinContainer,
39 getFirstHydratableChildWithinSuspenseInstance,
40 hydrateInstance,
41 + diffHydratedPropsForDevWarnings,
42 + describeHydratableInstanceForDevWarnings,
43 hydrateTextInstance,
44 + diffHydratedTextForDevWarnings,
45 hydrateSuspenseInstance,
46 getNextHydratableInstanceAfterSuspenseInstance,
47 shouldDeleteUnhydratedTailInstances,
45 - didNotMatchHydratedContainerTextInstance,
46 - didNotMatchHydratedTextInstance,
47 - didNotHydrateInstanceWithinContainer,
48 - didNotHydrateInstanceWithinSuspenseInstance,
49 - didNotHydrateInstance,
50 - didNotFindHydratableInstanceWithinContainer,
51 - didNotFindHydratableTextInstanceWithinContainer,
52 - didNotFindHydratableSuspenseInstanceWithinContainer,
53 - didNotFindHydratableInstanceWithinSuspenseInstance,
54 - didNotFindHydratableTextInstanceWithinSuspenseInstance,
55 - didNotFindHydratableSuspenseInstanceWithinSuspenseInstance,
56 - didNotFindHydratableInstance,
57 - didNotFindHydratableTextInstance,
58 - didNotFindHydratableSuspenseInstance,
48 resolveSingletonInstance,
49 canHydrateInstance,
50 canHydrateTextInstance,
@@ -141,36 +130,103 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
130 return true;
131 }
132
133 +function warnForDeletedHydratableInstance(
134 + parentType: string,
135 + child: HydratableInstance,
136 +) {
137 + if (__DEV__) {
138 + const description = describeHydratableInstanceForDevWarnings(child);
139 + if (typeof description === 'string') {
140 + console.error(
141 + 'Did not expect server HTML to contain the text node "%s" in <%s>.',
142 + description,
143 + parentType,
144 + );
145 + } else {
146 + console.error(
147 + 'Did not expect server HTML to contain a <%s> in <%s>.',
148 + description.type,
149 + parentType,
150 + );
151 + }
152 + }
153 +}
154 +
155 +function warnForInsertedHydratedElement(parentType: string, tag: string) {
156 + if (__DEV__) {
157 + console.error(
158 + 'Expected server HTML to contain a matching <%s> in <%s>.',
159 + tag,
160 + parentType,
161 + );
162 + }
163 +}
164 +
165 +function warnForInsertedHydratedText(parentType: string, text: string) {
166 + if (__DEV__) {
167 + console.error(
168 + 'Expected server HTML to contain a matching text node for "%s" in <%s>.',
169 + text,
170 + parentType,
171 + );
172 + }
173 +}
174 +
175 +function warnForInsertedHydratedSuspense(parentType: string) {
176 + if (__DEV__) {
177 + console.error(
178 + 'Expected server HTML to contain a matching <%s> in <%s>.',
179 + 'Suspense',
180 + parentType,
181 + );
182 + }
183 +}
184 +
185 +export function errorHydratingContainer(parentContainer: Container): void {
186 + if (__DEV__) {
187 + // TODO: This gets logged by onRecoverableError, too, so we should be
188 + // able to remove it.
189 + console.error(
190 + 'An error occurred during hydration. The server HTML was replaced with client content.',
191 + );
192 + }
193 +}
194 +
195 function warnUnhydratedInstance(
196 returnFiber: Fiber,
197 instance: HydratableInstance,
198 ) {
199 if (__DEV__) {
200 + if (didWarnInvalidHydration) {
201 + return;
202 + }
203 + didWarnInvalidHydration = true;
204 +
205 switch (returnFiber.tag) {
206 case HostRoot: {
151 - didNotHydrateInstanceWithinContainer(
152 - returnFiber.stateNode.containerInfo,
153 - instance,
154 - );
207 + const description = describeHydratableInstanceForDevWarnings(instance);
208 + if (typeof description === 'string') {
209 + console.error(
210 + 'Did not expect server HTML to contain the text node "%s" in the root.',
211 + description,
212 + );
213 + } else {
214 + console.error(
215 + 'Did not expect server HTML to contain a <%s> in the root.',
216 + description.type,
217 + );
218 + }
219 break;
220 }
221 case HostSingleton:
222 case HostComponent: {
159 - didNotHydrateInstance(
160 - returnFiber.type,
161 - returnFiber.memoizedProps,
162 - returnFiber.stateNode,
163 - instance,
164 - );
223 + warnForDeletedHydratableInstance(returnFiber.type, instance);
224 break;
225 }
226 case SuspenseComponent: {
227 const suspenseState: SuspenseState = returnFiber.memoizedState;
228 if (suspenseState.dehydrated !== null)
170 - didNotHydrateInstanceWithinSuspenseInstance(
171 - suspenseState.dehydrated,
172 - instance,
173 - );
229 + warnForDeletedHydratableInstance('Suspense', instance);
230 break;
231 }
232 }
@@ -186,30 +242,33 @@ function warnNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
242 return;
243 }
244
245 + if (didWarnInvalidHydration) {
246 + return;
247 + }
248 + didWarnInvalidHydration = true;
249 +
250 switch (returnFiber.tag) {
251 case HostRoot: {
191 - const parentContainer = returnFiber.stateNode.containerInfo;
252 + // const parentContainer = returnFiber.stateNode.containerInfo;
253 switch (fiber.tag) {
254 case HostSingleton:
255 case HostComponent:
195 - const type = fiber.type;
196 - const props = fiber.pendingProps;
197 - didNotFindHydratableInstanceWithinContainer(
198 - parentContainer,
199 - type,
200 - props,
256 + console.error(
257 + 'Expected server HTML to contain a matching <%s> in the root.',
258 + fiber.type,
259 );
260 break;
261 case HostText:
262 const text = fiber.pendingProps;
205 - didNotFindHydratableTextInstanceWithinContainer(
206 - parentContainer,
263 + console.error(
264 + 'Expected server HTML to contain a matching text node for "%s" in the root.',
265 text,
266 );
267 break;
268 case SuspenseComponent:
211 - didNotFindHydratableSuspenseInstanceWithinContainer(
212 - parentContainer,
269 + console.error(
270 + 'Expected server HTML to contain a matching <%s> in the root.',
271 + 'Suspense',
272 );
273 break;
274 }
@@ -218,71 +277,44 @@ function warnNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
277 case HostSingleton:
278 case HostComponent: {
279 const parentType = returnFiber.type;
221 - const parentProps = returnFiber.memoizedProps;
222 - const parentInstance = returnFiber.stateNode;
280 + // const parentProps = returnFiber.memoizedProps;
281 + // const parentInstance = returnFiber.stateNode;
282 switch (fiber.tag) {
283 case HostSingleton:
284 case HostComponent: {
285 const type = fiber.type;
227 - const props = fiber.pendingProps;
228 - didNotFindHydratableInstance(
229 - parentType,
230 - parentProps,
231 - parentInstance,
232 - type,
233 - props,
234 - );
286 + warnForInsertedHydratedElement(parentType, type);
287 break;
288 }
289 case HostText: {
290 const text = fiber.pendingProps;
239 - didNotFindHydratableTextInstance(
240 - parentType,
241 - parentProps,
242 - parentInstance,
243 - text,
244 - );
291 + warnForInsertedHydratedText(parentType, text);
292 break;
293 }
294 case SuspenseComponent: {
248 - didNotFindHydratableSuspenseInstance(
249 - parentType,
250 - parentProps,
251 - parentInstance,
252 - );
295 + warnForInsertedHydratedSuspense(parentType);
296 break;
297 }
298 }
299 break;
300 }
301 case SuspenseComponent: {
259 - const suspenseState: SuspenseState = returnFiber.memoizedState;
260 - const parentInstance = suspenseState.dehydrated;
261 - if (parentInstance !== null)
262 - switch (fiber.tag) {
263 - case HostSingleton:
264 - case HostComponent:
265 - const type = fiber.type;
266 - const props = fiber.pendingProps;
267 - didNotFindHydratableInstanceWithinSuspenseInstance(
268 - parentInstance,
269 - type,
270 - props,
271 - );
272 - break;
273 - case HostText:
274 - const text = fiber.pendingProps;
275 - didNotFindHydratableTextInstanceWithinSuspenseInstance(
276 - parentInstance,
277 - text,
278 - );
279 - break;
280 - case SuspenseComponent:
281 - didNotFindHydratableSuspenseInstanceWithinSuspenseInstance(
282 - parentInstance,
283 - );
284 - break;
285 - }
302 + // const suspenseState: SuspenseState = returnFiber.memoizedState;
303 + // const parentInstance = suspenseState.dehydrated;
304 + switch (fiber.tag) {
305 + case HostSingleton:
306 + case HostComponent:
307 + const type = fiber.type;
308 + warnForInsertedHydratedElement('Suspense', type);
309 + break;
310 + case HostText:
311 + const text = fiber.pendingProps;
312 + warnForInsertedHydratedText('Suspense', text);
313 + break;
314 + case SuspenseComponent:
315 + warnForInsertedHydratedSuspense('Suspense');
316 + break;
317 + }
318 break;
319 }
320 default:
@@ -465,6 +497,9 @@ export function tryToClaimNextHydratableFormMarkerInstance(
497 return false;
498 }
499
500 +// Temp
501 +let didWarnInvalidHydration = false;
502 +
503 function prepareToHydrateHostInstance(
504 fiber: Fiber,
505 hostContext: HostContext,
@@ -477,15 +512,63 @@ function prepareToHydrateHostInstance(
512 }
513
514 const instance: Instance = fiber.stateNode;
480 - const shouldWarnIfMismatchDev = !didSuspendOrErrorDEV;
481 - hydrateInstance(
515 + if (__DEV__) {
516 + const shouldWarnIfMismatchDev = !didSuspendOrErrorDEV;
517 + if (shouldWarnIfMismatchDev) {
518 + const differences = diffHydratedPropsForDevWarnings(
519 + instance,
520 + fiber.type,
521 + fiber.memoizedProps,
522 + hostContext,
523 + );
524 + if (differences !== null) {
525 + if (differences.children != null && !didWarnInvalidHydration) {
526 + didWarnInvalidHydration = true;
527 + const serverValue = differences.children;
528 + const clientValue = fiber.memoizedProps.children;
529 + console.error(
530 + 'Text content did not match. Server: "%s" Client: "%s"',
531 + serverValue,
532 + clientValue,
533 + );
534 + }
535 + for (const propName in differences) {
536 + if (!differences.hasOwnProperty(propName)) {
537 + continue;
538 + }
539 + if (didWarnInvalidHydration) {
540 + break;
541 + }
542 + didWarnInvalidHydration = true;
543 + const serverValue = differences[propName];
544 + const clientValue = fiber.memoizedProps[propName];
545 + if (propName === 'children') {
546 + // Already handled above
547 + } else if (clientValue != null) {
548 + console.error(
549 + 'Prop `%s` did not match. Server: %s Client: %s',
550 + propName,
551 + JSON.stringify(serverValue),
552 + JSON.stringify(clientValue),
553 + );
554 + } else {
555 + console.error('Extra attribute from the server: %s', propName);
556 + }
557 + }
558 + }
559 + }
560 + }
561 +
562 + const didHydrate = hydrateInstance(
563 instance,
564 fiber.type,
565 fiber.memoizedProps,
566 hostContext,
567 fiber,
487 - shouldWarnIfMismatchDev,
568 );
569 + if (!didHydrate) {
570 + throw new Error('Text content does not match server-rendered HTML.');
571 + }
572 }
573
574 function prepareToHydrateHostTextInstance(fiber: Fiber): void {
@@ -499,45 +582,66 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): void {
582 const textInstance: TextInstance = fiber.stateNode;
583 const textContent: string = fiber.memoizedProps;
584 const shouldWarnIfMismatchDev = !didSuspendOrErrorDEV;
502 - const textIsDifferent = hydrateTextInstance(
503 - textInstance,
504 - textContent,
505 - fiber,
506 - shouldWarnIfMismatchDev,
507 - );
508 - if (textIsDifferent) {
509 - // We assume that prepareToHydrateHostTextInstance is called in a context where the
510 - // hydration parent is the parent host component of this host text.
511 - const returnFiber = hydrationParentFiber;
512 - if (returnFiber !== null) {
513 - switch (returnFiber.tag) {
514 - case HostRoot: {
515 - const parentContainer = returnFiber.stateNode.containerInfo;
516 - didNotMatchHydratedContainerTextInstance(
517 - parentContainer,
518 - textInstance,
519 - textContent,
520 - shouldWarnIfMismatchDev,
521 - );
522 - break;
585 + let parentProps = null;
586 + // We assume that prepareToHydrateHostTextInstance is called in a context where the
587 + // hydration parent is the parent host component of this host text.
588 + const returnFiber = hydrationParentFiber;
589 + if (returnFiber !== null) {
590 + switch (returnFiber.tag) {
591 + case HostRoot: {
592 + if (__DEV__) {
593 + if (shouldWarnIfMismatchDev) {
594 + const difference = diffHydratedTextForDevWarnings(
595 + textInstance,
596 + textContent,
597 + parentProps,
598 + );
599 + if (difference !== null && !didWarnInvalidHydration) {
600 + didWarnInvalidHydration = true;
601 + console.error(
602 + 'Text content did not match. Server: "%s" Client: "%s"',
603 + difference,
604 + textContent,
605 + );
606 + }
607 + }
608 }
524 - case HostSingleton:
525 - case HostComponent: {
526 - const parentType = returnFiber.type;
527 - const parentProps = returnFiber.memoizedProps;
528 - const parentInstance = returnFiber.stateNode;
529 - didNotMatchHydratedTextInstance(
530 - parentType,
531 - parentProps,
532 - parentInstance,
533 - textInstance,
534 - textContent,
535 - shouldWarnIfMismatchDev,
536 - );
537 - break;
609 + break;
610 + }
611 + case HostSingleton:
612 + case HostComponent: {
613 + parentProps = returnFiber.memoizedProps;
614 + if (__DEV__) {
615 + if (shouldWarnIfMismatchDev) {
616 + const difference = diffHydratedTextForDevWarnings(
617 + textInstance,
618 + textContent,
619 + parentProps,
620 + );
621 + if (difference !== null && !didWarnInvalidHydration) {
622 + didWarnInvalidHydration = true;
623 + console.error(
624 + 'Text content did not match. Server: "%s" Client: "%s"',
625 + difference,
626 + textContent,
627 + );
628 + }
629 + }
630 }
631 + break;
632 }
633 }
634 + // TODO: What if it's a SuspenseInstance?
635 + }
636 +
637 + const didHydrate = hydrateTextInstance(
638 + textInstance,
639 + textContent,
640 + fiber,
641 + parentProps,
642 + );
643 + if (!didHydrate) {
644 + throw new Error('Text content does not match server-rendered HTML.');
645 }
646 }
647
packages/react-reconciler/src/ReactFiberWorkLoop.js
+6 -3
@@ -70,15 +70,18 @@ import {
70 noTimeout,
71 afterActiveInstanceBlur,
72 getCurrentEventPriority,
73 - errorHydratingContainer,
73 startSuspendingCommit,
74 waitForCommitToBeReady,
75 preloadInstance,
76 + supportsHydration,
77 } from './ReactFiberConfig';
78
79 import {createWorkInProgress, resetWorkInProgress} from './ReactFiber';
80 import {isRootDehydrated} from './ReactFiberShellHydration';
81 -import {getIsHydrating} from './ReactFiberHydrationContext';
81 +import {
82 + getIsHydrating,
83 + errorHydratingContainer,
84 +} from './ReactFiberHydrationContext';
85 import {
86 NoMode,
87 ProfileMode,
@@ -1003,7 +1006,7 @@ function recoverFromConcurrentError(
1006 // Before rendering again, save the errors from the previous attempt.
1007 const errorsFromFirstAttempt = workInProgressRootConcurrentErrors;
1008
1006 - const wasRootDehydrated = isRootDehydrated(root);
1009 + const wasRootDehydrated = supportsHydration && isRootDehydrated(root);
1010 if (wasRootDehydrated) {
1011 // The shell failed to hydrate. Set a flag to force a client rendering
1012 // during the next attempt. To do this, we call prepareFreshStack now
packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+6 -28
@@ -164,34 +164,12 @@ export const clearSuspenseBoundaryFromContainer =
164 $$$config.clearSuspenseBoundaryFromContainer;
165 export const shouldDeleteUnhydratedTailInstances =
166 $$$config.shouldDeleteUnhydratedTailInstances;
167 -export const didNotMatchHydratedContainerTextInstance =
168 - $$$config.didNotMatchHydratedContainerTextInstance;
169 -export const didNotMatchHydratedTextInstance =
170 - $$$config.didNotMatchHydratedTextInstance;
171 -export const didNotHydrateInstanceWithinContainer =
172 - $$$config.didNotHydrateInstanceWithinContainer;
173 -export const didNotHydrateInstanceWithinSuspenseInstance =
174 - $$$config.didNotHydrateInstanceWithinSuspenseInstance;
175 -export const didNotHydrateInstance = $$$config.didNotHydrateInstance;
176 -export const didNotFindHydratableInstanceWithinContainer =
177 - $$$config.didNotFindHydratableInstanceWithinContainer;
178 -export const didNotFindHydratableTextInstanceWithinContainer =
179 - $$$config.didNotFindHydratableTextInstanceWithinContainer;
180 -export const didNotFindHydratableSuspenseInstanceWithinContainer =
181 - $$$config.didNotFindHydratableSuspenseInstanceWithinContainer;
182 -export const didNotFindHydratableInstanceWithinSuspenseInstance =
183 - $$$config.didNotFindHydratableInstanceWithinSuspenseInstance;
184 -export const didNotFindHydratableTextInstanceWithinSuspenseInstance =
185 - $$$config.didNotFindHydratableTextInstanceWithinSuspenseInstance;
186 -export const didNotFindHydratableSuspenseInstanceWithinSuspenseInstance =
187 - $$$config.didNotFindHydratableSuspenseInstanceWithinSuspenseInstance;
188 -export const didNotFindHydratableInstance =
189 - $$$config.didNotFindHydratableInstance;
190 -export const didNotFindHydratableTextInstance =
191 - $$$config.didNotFindHydratableTextInstance;
192 -export const didNotFindHydratableSuspenseInstance =
193 - $$$config.didNotFindHydratableSuspenseInstance;
194 -export const errorHydratingContainer = $$$config.errorHydratingContainer;
167 +export const diffHydratedPropsForDevWarnings =
168 + $$$config.diffHydratedPropsForDevWarnings;
169 +export const diffHydratedTextForDevWarnings =
170 + $$$config.diffHydratedTextForDevWarnings;
171 +export const describeHydratableInstanceForDevWarnings =
172 + $$$config.describeHydratableInstanceForDevWarnings;
173 export const validateHydratableInstance = $$$config.validateHydratableInstance;
174 export const validateHydratableTextInstance =
175 $$$config.validateHydratableTextInstance;
scripts/jest/shouldIgnoreConsoleError.js
+1 -1
@@ -32,7 +32,7 @@ module.exports = function shouldIgnoreConsoleError(
32 if (
33 TODO_ignoreHydrationErrors &&
34 format.indexOf(
35 - 'An error occurred during hydration. The server HTML was replaced with client content in'
35 + 'An error occurred during hydration. The server HTML was replaced with client content'
36 ) !== -1
37 ) {
38 // This also gets logged by onRecoverableError, so we can ignore it.