Skip to content

Commit 2689cb7

Browse files
committed
Proof-of-concept hydrate warning diff (facebook#10085)
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
1 parent 5c660e6 commit 2689cb7

File tree

6 files changed

+154
-42
lines changed

6 files changed

+154
-42
lines changed

fixtures/ssr/src/components/SSRMismatchTest.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,38 @@ const testCases = [
196196
{
197197
key: 'ssr-warnForInsertedHydratedElement-didNotFindHydratableInstance',
198198
renderServer: () => (
199-
<div>
200-
<em>SSRMismatchTest default text</em>
199+
<div className="SSRMismatchTest__wrapper">
200+
<span className="SSRMismatchTest__1">1</span>
201+
<span className="SSRMismatchTest__2">2</span>
202+
<span className="SSRMismatchTest__3">3</span>
203+
<span className="SSRMismatchTest__4">4</span>
204+
<span className="SSRMismatchTest__5">5</span>
205+
<span className="SSRMismatchTest__6">6</span>
206+
<strong> SSRMismatchTest default text </strong>
207+
<span className="SSRMismatchTest__7">7</span>
208+
<span className="SSRMismatchTest__8">8</span>
209+
<span className="SSRMismatchTest__9">9</span>
210+
<span className="SSRMismatchTest__10">10</span>
211+
<span className="SSRMismatchTest__11">11</span>
212+
<span className="SSRMismatchTest__12">12</span>
201213
</div>
202214
),
203215
renderBrowser: () => (
204216
// The inner element type is different from the server render, but the inner text is the same.
205-
<div>
206-
<p>SSRMismatchTest default text</p>
217+
<div className="SSRMismatchTest__wrapper">
218+
<span className="SSRMismatchTest__1">1</span>
219+
<span className="SSRMismatchTest__2">2</span>
220+
<span className="SSRMismatchTest__3">3</span>
221+
<span className="SSRMismatchTest__4">4</span>
222+
<span className="SSRMismatchTest__5">5</span>
223+
<span className="SSRMismatchTest__6">6</span>
224+
<em> SSRMismatchTest default text </em>
225+
<span className="SSRMismatchTest__7">7</span>
226+
<span className="SSRMismatchTest__8">8</span>
227+
<span className="SSRMismatchTest__9">9</span>
228+
<span className="SSRMismatchTest__10">10</span>
229+
<span className="SSRMismatchTest__11">11</span>
230+
<span className="SSRMismatchTest__12">12</span>
207231
</div>
208232
),
209233
},

packages/react-dom/src/__tests__/ReactMount-test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,14 @@ describe('ReactMount', () => {
281281
),
282282
).toWarnDev(
283283
'Expected server HTML to contain a matching <div> in <div>.\n' +
284+
' <div>\n' +
285+
' nested\n' +
286+
' \n' +
287+
'- \n' +
288+
'+ <div />\n' +
289+
' <p>children text</p>\n' +
290+
' </div>\n' +
291+
'\n' +
284292
' in div (at **)\n' +
285293
' in Component (at **)',
286294
);
@@ -507,6 +515,11 @@ describe('ReactMount', () => {
507515
ReactDOM.hydrate(<span>SSRMismatchTest default text</span>, div),
508516
).toWarnDev(
509517
'Expected server HTML to contain a matching <span> in <div>.\n' +
518+
' <div>\n' +
519+
'- SSRMismatchTest default text\n' +
520+
'+ <span />\n' +
521+
' </div>\n' +
522+
'\n' +
510523
' in span (at **)',
511524
);
512525
});
@@ -531,6 +544,11 @@ describe('ReactMount', () => {
531544
),
532545
).toWarnDev(
533546
'Expected server HTML to contain a matching <p> in <div>.\n' +
547+
' <div>\n' +
548+
'- <em>SSRMismatchTest default text</em>\n' +
549+
'+ <p />\n' +
550+
' </div>\n' +
551+
'\n' +
534552
' in p (at **)\n' +
535553
' in div (at **)',
536554
);

packages/react-dom/src/client/ReactDOMFiberComponent.js

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ import {
3939
shouldRemoveAttribute,
4040
} from '../shared/DOMProperty';
4141
import assertValidProps from '../shared/assertValidProps';
42-
import {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE} from '../shared/HTMLNodeType';
42+
import {
43+
DOCUMENT_NODE,
44+
DOCUMENT_FRAGMENT_NODE,
45+
ELEMENT_NODE,
46+
} from '../shared/HTMLNodeType';
4347
import isCustomComponent from '../shared/isCustomComponent';
4448
import possibleStandardNames from '../shared/possibleStandardNames';
4549
import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
@@ -1159,17 +1163,75 @@ export function warnForInsertedHydratedElement(
11591163
parentNode: Element | Document,
11601164
tag: string,
11611165
props: Object,
1166+
index: number,
11621167
) {
11631168
if (__DEV__) {
11641169
if (didWarnInvalidHydration) {
11651170
return;
11661171
}
11671172
didWarnInvalidHydration = true;
1173+
let htmlContext = [];
1174+
const ic = parentNode.childNodes.length;
1175+
const parentNodeName = parentNode.nodeName.toLowerCase();
1176+
if (parentNode.nodeType === ELEMENT_NODE) {
1177+
// $FlowFixMe https://github.com/facebook/flow/issues/1032
1178+
const parentElement = (parentNode: Element);
1179+
htmlContext.push(
1180+
' <' +
1181+
parentNodeName +
1182+
(parentElement.className
1183+
? ' className="' + parentElement.className + '"'
1184+
: '') +
1185+
'>',
1186+
);
1187+
} else {
1188+
htmlContext.push(' <' + parentNodeName + '>');
1189+
}
1190+
if (index - 5 > 0) {
1191+
htmlContext.push(' …');
1192+
}
1193+
for (let i = index - 5; i <= index + 5; ++i) {
1194+
if (i >= 0 && i < ic) {
1195+
const childNode = parentNode.childNodes[i];
1196+
const childNodeName = childNode.nodeName.toLowerCase();
1197+
const diffPrefix = i === index ? '- ' : ' ';
1198+
if (childNode.nodeType === ELEMENT_NODE) {
1199+
// $FlowFixMe https://github.com/facebook/flow/issues/1032
1200+
const childElement = (childNode: Element);
1201+
htmlContext.push(
1202+
diffPrefix +
1203+
'<' +
1204+
childNodeName +
1205+
(childElement.className
1206+
? ' className="' + childElement.className + '"'
1207+
: '') +
1208+
(childElement.textContent
1209+
? '>' + childElement.textContent + '</' + childNodeName + '>'
1210+
: ' />'),
1211+
);
1212+
} else {
1213+
htmlContext.push(diffPrefix + childNode.textContent);
1214+
}
1215+
if (i === index) {
1216+
htmlContext.push(
1217+
'+ <' +
1218+
tag +
1219+
(props.className ? ' className="' + props.className + '"' : '') +
1220+
' />',
1221+
);
1222+
}
1223+
}
1224+
}
1225+
if (index + 5 < ic - 1) {
1226+
htmlContext.push(' …');
1227+
}
1228+
htmlContext.push(' </' + parentNodeName + '>');
11681229
warning(
11691230
false,
1170-
'Expected server HTML to contain a matching <%s> in <%s>.%s',
1231+
'Expected server HTML to contain a matching <%s> in <%s>.%s%s',
11711232
tag,
1172-
parentNode.nodeName.toLowerCase(),
1233+
parentNodeName,
1234+
'\n' + htmlContext.join('\n') + '\n',
11731235
getStack(),
11741236
);
11751237
}

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -542,9 +542,10 @@ export function didNotFindHydratableContainerInstance(
542542
parentContainer: Container,
543543
type: string,
544544
props: Props,
545+
index: number,
545546
) {
546547
if (__DEV__) {
547-
warnForInsertedHydratedElement(parentContainer, type, props);
548+
warnForInsertedHydratedElement(parentContainer, type, props, index);
548549
}
549550
}
550551

@@ -563,9 +564,10 @@ export function didNotFindHydratableInstance(
563564
parentInstance: Instance,
564565
type: string,
565566
props: Props,
567+
index: number,
566568
) {
567569
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
568-
warnForInsertedHydratedElement(parentInstance, type, props);
570+
warnForInsertedHydratedElement(parentInstance, type, props, index);
569571
}
570572
}
571573

packages/react-reconciler/src/ReactFiberHydrationContext.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,12 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
109109
case HostComponent:
110110
const type = fiber.type;
111111
const props = fiber.pendingProps;
112-
didNotFindHydratableContainerInstance(parentContainer, type, props);
112+
didNotFindHydratableContainerInstance(
113+
parentContainer,
114+
type,
115+
props,
116+
fiber.index,
117+
);
113118
break;
114119
case HostText:
115120
const text = fiber.pendingProps;
@@ -132,6 +137,7 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
132137
parentInstance,
133138
type,
134139
props,
140+
fiber.index,
135141
);
136142
break;
137143
case HostText:

scripts/rollup/results.json

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,22 @@
4646
"filename": "react-dom.development.js",
4747
"bundleType": "UMD_DEV",
4848
"packageName": "react-dom",
49-
"size": 641404,
50-
"gzip": 149333
49+
"size": 643066,
50+
"gzip": 149750
5151
},
5252
{
5353
"filename": "react-dom.production.min.js",
5454
"bundleType": "UMD_PROD",
5555
"packageName": "react-dom",
56-
"size": 96422,
57-
"gzip": 31235
56+
"size": 96424,
57+
"gzip": 31236
5858
},
5959
{
6060
"filename": "react-dom.development.js",
6161
"bundleType": "NODE_DEV",
6262
"packageName": "react-dom",
63-
"size": 625395,
64-
"gzip": 145233
63+
"size": 627057,
64+
"gzip": 145658
6565
},
6666
{
6767
"filename": "react-dom.production.min.js",
@@ -221,8 +221,8 @@
221221
"filename": "react-art.development.js",
222222
"bundleType": "UMD_DEV",
223223
"packageName": "react-art",
224-
"size": 417688,
225-
"gzip": 93226
224+
"size": 417714,
225+
"gzip": 93230
226226
},
227227
{
228228
"filename": "react-art.production.min.js",
@@ -235,8 +235,8 @@
235235
"filename": "react-art.development.js",
236236
"bundleType": "NODE_DEV",
237237
"packageName": "react-art",
238-
"size": 341761,
239-
"gzip": 73860
238+
"size": 341787,
239+
"gzip": 73869
240240
},
241241
{
242242
"filename": "react-art.production.min.js",
@@ -291,8 +291,8 @@
291291
"filename": "react-test-renderer.development.js",
292292
"bundleType": "UMD_DEV",
293293
"packageName": "react-test-renderer",
294-
"size": 348566,
295-
"gzip": 75373
294+
"size": 348592,
295+
"gzip": 75379
296296
},
297297
{
298298
"filename": "react-test-renderer.production.min.js",
@@ -305,8 +305,8 @@
305305
"filename": "react-test-renderer.development.js",
306306
"bundleType": "NODE_DEV",
307307
"packageName": "react-test-renderer",
308-
"size": 339177,
309-
"gzip": 72574
308+
"size": 339203,
309+
"gzip": 72584
310310
},
311311
{
312312
"filename": "react-test-renderer.production.min.js",
@@ -375,8 +375,8 @@
375375
"filename": "react-reconciler.development.js",
376376
"bundleType": "NODE_DEV",
377377
"packageName": "react-reconciler",
378-
"size": 332072,
379-
"gzip": 70323
378+
"size": 332098,
379+
"gzip": 70332
380380
},
381381
{
382382
"filename": "react-reconciler.production.min.js",
@@ -389,8 +389,8 @@
389389
"filename": "react-reconciler-persistent.development.js",
390390
"bundleType": "NODE_DEV",
391391
"packageName": "react-reconciler",
392-
"size": 330592,
393-
"gzip": 69702
392+
"size": 330618,
393+
"gzip": 69716
394394
},
395395
{
396396
"filename": "react-reconciler-persistent.production.min.js",
@@ -515,8 +515,8 @@
515515
"filename": "ReactDOM-dev.js",
516516
"bundleType": "FB_WWW_DEV",
517517
"packageName": "react-dom",
518-
"size": 635171,
519-
"gzip": 144610
518+
"size": 637215,
519+
"gzip": 145069
520520
},
521521
{
522522
"filename": "ReactDOM-prod.js",
@@ -564,8 +564,8 @@
564564
"filename": "ReactART-dev.js",
565565
"bundleType": "FB_WWW_DEV",
566566
"packageName": "react-art",
567-
"size": 334212,
568-
"gzip": 69692
567+
"size": 334322,
568+
"gzip": 69699
569569
},
570570
{
571571
"filename": "ReactART-prod.js",
@@ -578,8 +578,8 @@
578578
"filename": "ReactNativeRenderer-dev.js",
579579
"bundleType": "RN_FB_DEV",
580580
"packageName": "react-native-renderer",
581-
"size": 468774,
582-
"gzip": 102456
581+
"size": 468884,
582+
"gzip": 102463
583583
},
584584
{
585585
"filename": "ReactNativeRenderer-prod.js",
@@ -592,8 +592,8 @@
592592
"filename": "ReactNativeRenderer-dev.js",
593593
"bundleType": "RN_OSS_DEV",
594594
"packageName": "react-native-renderer",
595-
"size": 468428,
596-
"gzip": 102391
595+
"size": 468538,
596+
"gzip": 102397
597597
},
598598
{
599599
"filename": "ReactNativeRenderer-prod.js",
@@ -606,8 +606,8 @@
606606
"filename": "ReactFabric-dev.js",
607607
"bundleType": "RN_FB_DEV",
608608
"packageName": "react-native-renderer",
609-
"size": 459473,
610-
"gzip": 100165
609+
"size": 459583,
610+
"gzip": 100172
611611
},
612612
{
613613
"filename": "ReactFabric-prod.js",
@@ -620,8 +620,8 @@
620620
"filename": "ReactFabric-dev.js",
621621
"bundleType": "RN_OSS_DEV",
622622
"packageName": "react-native-renderer",
623-
"size": 459510,
624-
"gzip": 100183
623+
"size": 459620,
624+
"gzip": 100190
625625
},
626626
{
627627
"filename": "ReactFabric-prod.js",
@@ -634,8 +634,8 @@
634634
"filename": "ReactTestRenderer-dev.js",
635635
"bundleType": "FB_WWW_DEV",
636636
"packageName": "react-test-renderer",
637-
"size": 345219,
638-
"gzip": 72154
637+
"size": 345329,
638+
"gzip": 72160
639639
},
640640
{
641641
"filename": "ReactShallowRenderer-dev.js",

0 commit comments

Comments
 (0)