Skip to content

Commit

Permalink
Proof-of-concept hydrate warning diff (facebook#10085)
Browse files Browse the repository at this point in the history
Example warning:
```
Warning: Expected server HTML to contain a matching <em> in <div>.
 <div className="SSRMismatchTest__wrapper">
   …
   <span className="SSRMismatchTest__2">2</span>
   <span className="SSRMismatchTest__3">3</span>
   <span className="SSRMismatchTest__4">4</span>
   <span className="SSRMismatchTest__5">5</span>
   <span className="SSRMismatchTest__6">6</span>
-  <strong> SSRMismatchTest default text </strong>
+  <em />
   <span className="SSRMismatchTest__7">7</span>
   <span className="SSRMismatchTest__8">8</span>
   <span className="SSRMismatchTest__9">9</span>
   <span className="SSRMismatchTest__10">10</span>
   <span className="SSRMismatchTest__11">11</span>
   …
 </div>

    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
  • Loading branch information
sompylasar committed Jun 9, 2018
1 parent 5c660e6 commit 2689cb7
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 42 deletions.
32 changes: 28 additions & 4 deletions fixtures/ssr/src/components/SSRMismatchTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,14 +196,38 @@ const testCases = [
{
key: 'ssr-warnForInsertedHydratedElement-didNotFindHydratableInstance',
renderServer: () => (
<div>
<em>SSRMismatchTest default text</em>
<div className="SSRMismatchTest__wrapper">
<span className="SSRMismatchTest__1">1</span>
<span className="SSRMismatchTest__2">2</span>
<span className="SSRMismatchTest__3">3</span>
<span className="SSRMismatchTest__4">4</span>
<span className="SSRMismatchTest__5">5</span>
<span className="SSRMismatchTest__6">6</span>
<strong> SSRMismatchTest default text </strong>
<span className="SSRMismatchTest__7">7</span>
<span className="SSRMismatchTest__8">8</span>
<span className="SSRMismatchTest__9">9</span>
<span className="SSRMismatchTest__10">10</span>
<span className="SSRMismatchTest__11">11</span>
<span className="SSRMismatchTest__12">12</span>
</div>
),
renderBrowser: () => (
// The inner element type is different from the server render, but the inner text is the same.
<div>
<p>SSRMismatchTest default text</p>
<div className="SSRMismatchTest__wrapper">
<span className="SSRMismatchTest__1">1</span>
<span className="SSRMismatchTest__2">2</span>
<span className="SSRMismatchTest__3">3</span>
<span className="SSRMismatchTest__4">4</span>
<span className="SSRMismatchTest__5">5</span>
<span className="SSRMismatchTest__6">6</span>
<em> SSRMismatchTest default text </em>
<span className="SSRMismatchTest__7">7</span>
<span className="SSRMismatchTest__8">8</span>
<span className="SSRMismatchTest__9">9</span>
<span className="SSRMismatchTest__10">10</span>
<span className="SSRMismatchTest__11">11</span>
<span className="SSRMismatchTest__12">12</span>
</div>
),
},
Expand Down
18 changes: 18 additions & 0 deletions packages/react-dom/src/__tests__/ReactMount-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@ describe('ReactMount', () => {
),
).toWarnDev(
'Expected server HTML to contain a matching <div> in <div>.\n' +
' <div>\n' +
' nested\n' +
' \n' +
'- \n' +
'+ <div />\n' +
' <p>children text</p>\n' +
' </div>\n' +
'\n' +
' in div (at **)\n' +
' in Component (at **)',
);
Expand Down Expand Up @@ -507,6 +515,11 @@ describe('ReactMount', () => {
ReactDOM.hydrate(<span>SSRMismatchTest default text</span>, div),
).toWarnDev(
'Expected server HTML to contain a matching <span> in <div>.\n' +
' <div>\n' +
'- SSRMismatchTest default text\n' +
'+ <span />\n' +
' </div>\n' +
'\n' +
' in span (at **)',
);
});
Expand All @@ -531,6 +544,11 @@ describe('ReactMount', () => {
),
).toWarnDev(
'Expected server HTML to contain a matching <p> in <div>.\n' +
' <div>\n' +
'- <em>SSRMismatchTest default text</em>\n' +
'+ <p />\n' +
' </div>\n' +
'\n' +
' in p (at **)\n' +
' in div (at **)',
);
Expand Down
68 changes: 65 additions & 3 deletions packages/react-dom/src/client/ReactDOMFiberComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 + '</' + childNodeName + '>'
: ' />'),
);
} 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(' </' + parentNodeName + '>');
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(),
);
}
Expand Down
6 changes: 4 additions & 2 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -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);
}
}

Expand Down
8 changes: 7 additions & 1 deletion packages/react-reconciler/src/ReactFiberHydrationContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -132,6 +137,7 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
parentInstance,
type,
props,
fiber.index,
);
break;
case HostText:
Expand Down
64 changes: 32 additions & 32 deletions scripts/rollup/results.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down

0 comments on commit 2689cb7

Please sign in to comment.