From 2363589041eb0258f073b22f0f05c1c98bb1fa72 Mon Sep 17 00:00:00 2001 From: Ivan Babak Date: Sun, 18 Feb 2018 03:34:48 -0800 Subject: [PATCH] Proof-of-concept hydrate warning diff (#10085) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Example warning: ``` Warning: Expected server HTML to contain a matching in
.
2 3 4 5 6 - SSRMismatchTest default text + 7 8 9 10 11
in em (at SSRMismatchTest.js:224) in div (at SSRMismatchTest.js:217) in div (at SSRMismatchTest.js:283) in SSRMismatchTest (at App.js:14) in div (at App.js:11) in body (at Chrome.js:17) in html (at Chrome.js:9) in Chrome (at App.js:10) in App (at index.js:8) ``` https://user-images.githubusercontent.com/498274/36351251-d04e8fca-145b-11e8-995d-389e0ae99456.png --- .../ssr/src/components/SSRMismatchTest.js | 32 +++++++-- .../src/__tests__/ReactMount-test.js | 18 +++++ .../src/client/ReactDOMFiberComponent.js | 68 ++++++++++++++++++- .../src/client/ReactDOMHostConfig.js | 6 +- .../src/ReactFiberHydrationContext.js | 8 ++- scripts/rollup/results.json | 64 ++++++++--------- 6 files changed, 154 insertions(+), 42 deletions(-) diff --git a/fixtures/ssr/src/components/SSRMismatchTest.js b/fixtures/ssr/src/components/SSRMismatchTest.js index 22113477989d8..74918fa2cccae 100644 --- a/fixtures/ssr/src/components/SSRMismatchTest.js +++ b/fixtures/ssr/src/components/SSRMismatchTest.js @@ -196,14 +196,38 @@ const testCases = [ { key: 'ssr-warnForInsertedHydratedElement-didNotFindHydratableInstance', renderServer: () => ( -
- SSRMismatchTest default text +
+ 1 + 2 + 3 + 4 + 5 + 6 + SSRMismatchTest default text + 7 + 8 + 9 + 10 + 11 + 12
), renderBrowser: () => ( // The inner element type is different from the server render, but the inner text is the same. -
-

SSRMismatchTest default text

+
+ 1 + 2 + 3 + 4 + 5 + 6 + SSRMismatchTest default text + 7 + 8 + 9 + 10 + 11 + 12
), }, diff --git a/packages/react-dom/src/__tests__/ReactMount-test.js b/packages/react-dom/src/__tests__/ReactMount-test.js index 0734185914235..be55ecfdde76f 100644 --- a/packages/react-dom/src/__tests__/ReactMount-test.js +++ b/packages/react-dom/src/__tests__/ReactMount-test.js @@ -281,6 +281,14 @@ describe('ReactMount', () => { ), ).toWarnDev( 'Expected server HTML to contain a matching
in
.\n' + + '
\n' + + ' nested\n' + + ' \n' + + '- \n' + + '+
\n' + + '

children text

\n' + + '
\n' + + '\n' + ' in div (at **)\n' + ' in Component (at **)', ); @@ -507,6 +515,11 @@ describe('ReactMount', () => { ReactDOM.hydrate(SSRMismatchTest default text, div), ).toWarnDev( 'Expected server HTML to contain a matching in
.\n' + + '
\n' + + '- SSRMismatchTest default text\n' + + '+ \n' + + '
\n' + + '\n' + ' in span (at **)', ); }); @@ -531,6 +544,11 @@ describe('ReactMount', () => { ), ).toWarnDev( 'Expected server HTML to contain a matching

in

.\n' + + '
\n' + + '- SSRMismatchTest default text\n' + + '+

\n' + + '

\n' + + '\n' + ' in p (at **)\n' + ' in div (at **)', ); diff --git a/packages/react-dom/src/client/ReactDOMFiberComponent.js b/packages/react-dom/src/client/ReactDOMFiberComponent.js index 73846d8497789..54bf2b7c73f71 100644 --- a/packages/react-dom/src/client/ReactDOMFiberComponent.js +++ b/packages/react-dom/src/client/ReactDOMFiberComponent.js @@ -39,7 +39,11 @@ import { shouldRemoveAttribute, } from '../shared/DOMProperty'; import assertValidProps from '../shared/assertValidProps'; -import {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE} from '../shared/HTMLNodeType'; +import { + DOCUMENT_NODE, + DOCUMENT_FRAGMENT_NODE, + ELEMENT_NODE, +} from '../shared/HTMLNodeType'; import isCustomComponent from '../shared/isCustomComponent'; import possibleStandardNames from '../shared/possibleStandardNames'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; @@ -1159,17 +1163,75 @@ export function warnForInsertedHydratedElement( parentNode: Element | Document, tag: string, props: Object, + index: number, ) { if (__DEV__) { if (didWarnInvalidHydration) { return; } didWarnInvalidHydration = true; + let htmlContext = []; + const ic = parentNode.childNodes.length; + const parentNodeName = parentNode.nodeName.toLowerCase(); + if (parentNode.nodeType === ELEMENT_NODE) { + // $FlowFixMe https://github.com/facebook/flow/issues/1032 + const parentElement = (parentNode: Element); + htmlContext.push( + ' <' + + parentNodeName + + (parentElement.className + ? ' className="' + parentElement.className + '"' + : '') + + '>', + ); + } else { + htmlContext.push(' <' + parentNodeName + '>'); + } + if (index - 5 > 0) { + htmlContext.push(' …'); + } + for (let i = index - 5; i <= index + 5; ++i) { + if (i >= 0 && i < ic) { + const childNode = parentNode.childNodes[i]; + const childNodeName = childNode.nodeName.toLowerCase(); + const diffPrefix = i === index ? '- ' : ' '; + if (childNode.nodeType === ELEMENT_NODE) { + // $FlowFixMe https://github.com/facebook/flow/issues/1032 + const childElement = (childNode: Element); + htmlContext.push( + diffPrefix + + '<' + + childNodeName + + (childElement.className + ? ' className="' + childElement.className + '"' + : '') + + (childElement.textContent + ? '>' + childElement.textContent + '' + : ' />'), + ); + } else { + htmlContext.push(diffPrefix + childNode.textContent); + } + if (i === index) { + htmlContext.push( + '+ <' + + tag + + (props.className ? ' className="' + props.className + '"' : '') + + ' />', + ); + } + } + } + if (index + 5 < ic - 1) { + htmlContext.push(' …'); + } + htmlContext.push(' '); warning( false, - 'Expected server HTML to contain a matching <%s> in <%s>.%s', + 'Expected server HTML to contain a matching <%s> in <%s>.%s%s', tag, - parentNode.nodeName.toLowerCase(), + parentNodeName, + '\n' + htmlContext.join('\n') + '\n', getStack(), ); } diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index c19a2c7425ce9..8153d2b00a802 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -542,9 +542,10 @@ export function didNotFindHydratableContainerInstance( parentContainer: Container, type: string, props: Props, + index: number, ) { if (__DEV__) { - warnForInsertedHydratedElement(parentContainer, type, props); + warnForInsertedHydratedElement(parentContainer, type, props, index); } } @@ -563,9 +564,10 @@ export function didNotFindHydratableInstance( parentInstance: Instance, type: string, props: Props, + index: number, ) { if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { - warnForInsertedHydratedElement(parentInstance, type, props); + warnForInsertedHydratedElement(parentInstance, type, props, index); } } diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js index bc4b9eae80e77..ac4fc89ec66dc 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js @@ -109,7 +109,12 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { case HostComponent: const type = fiber.type; const props = fiber.pendingProps; - didNotFindHydratableContainerInstance(parentContainer, type, props); + didNotFindHydratableContainerInstance( + parentContainer, + type, + props, + fiber.index, + ); break; case HostText: const text = fiber.pendingProps; @@ -132,6 +137,7 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { parentInstance, type, props, + fiber.index, ); break; case HostText: diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index 644c175bc629c..6c7b335208dfe 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -46,22 +46,22 @@ "filename": "react-dom.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 641404, - "gzip": 149333 + "size": 643066, + "gzip": 149750 }, { "filename": "react-dom.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-dom", - "size": 96422, - "gzip": 31235 + "size": 96424, + "gzip": 31236 }, { "filename": "react-dom.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 625395, - "gzip": 145233 + "size": 627057, + "gzip": 145658 }, { "filename": "react-dom.production.min.js", @@ -221,8 +221,8 @@ "filename": "react-art.development.js", "bundleType": "UMD_DEV", "packageName": "react-art", - "size": 417688, - "gzip": 93226 + "size": 417714, + "gzip": 93230 }, { "filename": "react-art.production.min.js", @@ -235,8 +235,8 @@ "filename": "react-art.development.js", "bundleType": "NODE_DEV", "packageName": "react-art", - "size": 341761, - "gzip": 73860 + "size": 341787, + "gzip": 73869 }, { "filename": "react-art.production.min.js", @@ -291,8 +291,8 @@ "filename": "react-test-renderer.development.js", "bundleType": "UMD_DEV", "packageName": "react-test-renderer", - "size": 348566, - "gzip": 75373 + "size": 348592, + "gzip": 75379 }, { "filename": "react-test-renderer.production.min.js", @@ -305,8 +305,8 @@ "filename": "react-test-renderer.development.js", "bundleType": "NODE_DEV", "packageName": "react-test-renderer", - "size": 339177, - "gzip": 72574 + "size": 339203, + "gzip": 72584 }, { "filename": "react-test-renderer.production.min.js", @@ -375,8 +375,8 @@ "filename": "react-reconciler.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 332072, - "gzip": 70323 + "size": 332098, + "gzip": 70332 }, { "filename": "react-reconciler.production.min.js", @@ -389,8 +389,8 @@ "filename": "react-reconciler-persistent.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 330592, - "gzip": 69702 + "size": 330618, + "gzip": 69716 }, { "filename": "react-reconciler-persistent.production.min.js", @@ -515,8 +515,8 @@ "filename": "ReactDOM-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 635171, - "gzip": 144610 + "size": 637215, + "gzip": 145069 }, { "filename": "ReactDOM-prod.js", @@ -564,8 +564,8 @@ "filename": "ReactART-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-art", - "size": 334212, - "gzip": 69692 + "size": 334322, + "gzip": 69699 }, { "filename": "ReactART-prod.js", @@ -578,8 +578,8 @@ "filename": "ReactNativeRenderer-dev.js", "bundleType": "RN_FB_DEV", "packageName": "react-native-renderer", - "size": 468774, - "gzip": 102456 + "size": 468884, + "gzip": 102463 }, { "filename": "ReactNativeRenderer-prod.js", @@ -592,8 +592,8 @@ "filename": "ReactNativeRenderer-dev.js", "bundleType": "RN_OSS_DEV", "packageName": "react-native-renderer", - "size": 468428, - "gzip": 102391 + "size": 468538, + "gzip": 102397 }, { "filename": "ReactNativeRenderer-prod.js", @@ -606,8 +606,8 @@ "filename": "ReactFabric-dev.js", "bundleType": "RN_FB_DEV", "packageName": "react-native-renderer", - "size": 459473, - "gzip": 100165 + "size": 459583, + "gzip": 100172 }, { "filename": "ReactFabric-prod.js", @@ -620,8 +620,8 @@ "filename": "ReactFabric-dev.js", "bundleType": "RN_OSS_DEV", "packageName": "react-native-renderer", - "size": 459510, - "gzip": 100183 + "size": 459620, + "gzip": 100190 }, { "filename": "ReactFabric-prod.js", @@ -634,8 +634,8 @@ "filename": "ReactTestRenderer-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-test-renderer", - "size": 345219, - "gzip": 72154 + "size": 345329, + "gzip": 72160 }, { "filename": "ReactShallowRenderer-dev.js",