Skip to content

Commit a5e9a70

Browse files
committed
Proof-of-concept hydrate warning diff
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 832b294 commit a5e9a70

File tree

5 files changed

+122
-10
lines changed

5 files changed

+122
-10
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
@@ -38,7 +38,11 @@ import {
3838
shouldRemoveAttribute,
3939
} from '../shared/DOMProperty';
4040
import assertValidProps from '../shared/assertValidProps';
41-
import {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE} from '../shared/HTMLNodeType';
41+
import {
42+
DOCUMENT_NODE,
43+
DOCUMENT_FRAGMENT_NODE,
44+
ELEMENT_NODE,
45+
} from '../shared/HTMLNodeType';
4246
import isCustomComponent from '../shared/isCustomComponent';
4347
import possibleStandardNames from '../shared/possibleStandardNames';
4448
import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
@@ -1160,17 +1164,75 @@ export function warnForInsertedHydratedElement(
11601164
parentNode: Element | Document,
11611165
tag: string,
11621166
props: Object,
1167+
index: number,
11631168
) {
11641169
if (__DEV__) {
11651170
if (didWarnInvalidHydration) {
11661171
return;
11671172
}
11681173
didWarnInvalidHydration = true;
1174+
let htmlContext = [];
1175+
const ic = parentNode.childNodes.length;
1176+
const parentNodeName = parentNode.nodeName.toLowerCase();
1177+
if (parentNode.nodeType === ELEMENT_NODE) {
1178+
// $FlowFixMe https://github.com/facebook/flow/issues/1032
1179+
const parentElement = (parentNode: Element);
1180+
htmlContext.push(
1181+
' <' +
1182+
parentNodeName +
1183+
(parentElement.className
1184+
? ' className="' + parentElement.className + '"'
1185+
: '') +
1186+
'>',
1187+
);
1188+
} else {
1189+
htmlContext.push(' <' + parentNodeName + '>');
1190+
}
1191+
if (index - 5 > 0) {
1192+
htmlContext.push(' …');
1193+
}
1194+
for (let i = index - 5; i <= index + 5; ++i) {
1195+
if (i >= 0 && i < ic) {
1196+
const childNode = parentNode.childNodes[i];
1197+
const childNodeName = childNode.nodeName.toLowerCase();
1198+
const diffPrefix = i === index ? '- ' : ' ';
1199+
if (childNode.nodeType === ELEMENT_NODE) {
1200+
// $FlowFixMe https://github.com/facebook/flow/issues/1032
1201+
const childElement = (childNode: Element);
1202+
htmlContext.push(
1203+
diffPrefix +
1204+
'<' +
1205+
childNodeName +
1206+
(childElement.className
1207+
? ' className="' + childElement.className + '"'
1208+
: '') +
1209+
(childElement.textContent
1210+
? '>' + childElement.textContent + '</' + childNodeName + '>'
1211+
: ' />'),
1212+
);
1213+
} else {
1214+
htmlContext.push(diffPrefix + childNode.textContent);
1215+
}
1216+
if (i === index) {
1217+
htmlContext.push(
1218+
'+ <' +
1219+
tag +
1220+
(props.className ? ' className="' + props.className + '"' : '') +
1221+
' />',
1222+
);
1223+
}
1224+
}
1225+
}
1226+
if (index + 5 < ic - 1) {
1227+
htmlContext.push(' …');
1228+
}
1229+
htmlContext.push(' </' + parentNodeName + '>');
11691230
warning(
11701231
false,
1171-
'Expected server HTML to contain a matching <%s> in <%s>.%s',
1232+
'Expected server HTML to contain a matching <%s> in <%s>.%s%s',
11721233
tag,
1173-
parentNode.nodeName.toLowerCase(),
1234+
parentNodeName,
1235+
'\n' + htmlContext.join('\n') + '\n',
11741236
getStack(),
11751237
);
11761238
}

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:

0 commit comments

Comments
 (0)