Skip to content

Commit 44c32fc

Browse files
authored
Ignore CR/LF differences when warning about markup mismatch (#11119)
* Add regression test for CR-insensitive hydration * Normalize CR when comparing server HTML to DOM * Move tests in the file * Add a failing test for comparing attributes * Normalize CR for attributes too * Add a test case for CR in dangerouslySetInnerHTML * Undo the fix per feedback * Change the fix to be DEV-only and still patch up LF -> CR * Remove the dangerouslySetInnerHTML test It's always going to "pass" because we normalize HTML anyway. Except that it won't pass because we intentionally don't patch up dangerouslyInnerHTML. * Fix issue that Flow failed to catch * Add null character tests * Normalize both client and server value for the warning * Fix the bug * Normalize replacement character away as well * Fix outdated comment
1 parent 32ec797 commit 44c32fc

File tree

2 files changed

+115
-5
lines changed

2 files changed

+115
-5
lines changed

src/renderers/dom/fiber/ReactDOMFiberComponent.js

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,41 @@ if (__DEV__) {
7676
validateUnknownProperties(type, props);
7777
};
7878

79-
var warnForTextDifference = function(serverText: string, clientText: string) {
79+
// HTML parsing normalizes CR and CRLF to LF.
80+
// It also can turn \u0000 into \uFFFD inside attributes.
81+
// https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
82+
// If we have a mismatch, it might be caused by that.
83+
// We will still patch up in this case but not fire the warning.
84+
var NORMALIZE_NEWLINES_REGEX = /\r\n?/g;
85+
var NORMALIZE_NULL_AND_REPLACEMENT_REGEX = /\u0000|\uFFFD/g;
86+
87+
var normalizeMarkupForTextOrAttribute = function(markup: mixed): string {
88+
const markupString = typeof markup === 'string'
89+
? markup
90+
: '' + (markup: any);
91+
return markupString
92+
.replace(NORMALIZE_NEWLINES_REGEX, '\n')
93+
.replace(NORMALIZE_NULL_AND_REPLACEMENT_REGEX, '');
94+
};
95+
96+
var warnForTextDifference = function(
97+
serverText: string,
98+
clientText: string | number,
99+
) {
80100
if (didWarnInvalidHydration) {
81101
return;
82102
}
103+
const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText);
104+
const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText);
105+
if (normalizedServerText === normalizedClientText) {
106+
return;
107+
}
83108
didWarnInvalidHydration = true;
84109
warning(
85110
false,
86111
'Text content did not match. Server: "%s" Client: "%s"',
87-
serverText,
88-
clientText,
112+
normalizedServerText,
113+
normalizedClientText,
89114
);
90115
};
91116

@@ -97,13 +122,22 @@ if (__DEV__) {
97122
if (didWarnInvalidHydration) {
98123
return;
99124
}
125+
const normalizedClientValue = normalizeMarkupForTextOrAttribute(
126+
clientValue,
127+
);
128+
const normalizedServerValue = normalizeMarkupForTextOrAttribute(
129+
serverValue,
130+
);
131+
if (normalizedServerValue === normalizedClientValue) {
132+
return;
133+
}
100134
didWarnInvalidHydration = true;
101135
warning(
102136
false,
103137
'Prop `%s` did not match. Server: %s Client: %s',
104138
propName,
105-
JSON.stringify(serverValue),
106-
JSON.stringify(clientValue),
139+
JSON.stringify(normalizedServerValue),
140+
JSON.stringify(normalizedClientValue),
107141
);
108142
};
109143

src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,6 +1574,82 @@ describe('ReactDOMServerIntegration', () => {
15741574
});
15751575
});
15761576

1577+
describe('carriage return and null character', () => {
1578+
// HTML parsing normalizes CR and CRLF to LF.
1579+
// It also ignores null character.
1580+
// https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
1581+
// If we have a mismatch, it might be caused by that (and should not be reported).
1582+
// We won't be patching up in this case as that matches our past behavior.
1583+
1584+
itRenders(
1585+
'an element with one text child with special characters',
1586+
async render => {
1587+
const e = await render(<div>{'foo\rbar\r\nbaz\nqux\u0000'}</div>);
1588+
if (render === serverRender || render === streamRender) {
1589+
expect(e.childNodes.length).toBe(1);
1590+
// Everything becomes LF when parsed from server HTML.
1591+
// Null character is ignored.
1592+
expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar\nbaz\nqux');
1593+
} else {
1594+
expect(e.childNodes.length).toBe(1);
1595+
// Client rendering (or hydration) uses JS value with CR.
1596+
// Null character stays.
1597+
expectNode(
1598+
e.childNodes[0],
1599+
TEXT_NODE_TYPE,
1600+
'foo\rbar\r\nbaz\nqux\u0000',
1601+
);
1602+
}
1603+
},
1604+
);
1605+
1606+
itRenders(
1607+
'an element with two text children with special characters',
1608+
async render => {
1609+
const e = await render(<div>{'foo\rbar'}{'\r\nbaz\nqux\u0000'}</div>);
1610+
if (render === serverRender || render === streamRender) {
1611+
// We have three nodes because there is a comment between them.
1612+
expect(e.childNodes.length).toBe(3);
1613+
// Everything becomes LF when parsed from server HTML.
1614+
// Null character is ignored.
1615+
expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar');
1616+
expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\nbaz\nqux');
1617+
} else if (render === clientRenderOnServerString) {
1618+
// We have three nodes because there is a comment between them.
1619+
expect(e.childNodes.length).toBe(3);
1620+
// Hydration uses JS value with CR and null character.
1621+
expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar');
1622+
expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000');
1623+
} else {
1624+
expect(e.childNodes.length).toBe(2);
1625+
// Client rendering uses JS value with CR and null character.
1626+
expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar');
1627+
expectNode(e.childNodes[1], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000');
1628+
}
1629+
},
1630+
);
1631+
1632+
itRenders(
1633+
'an element with an attribute value with special characters',
1634+
async render => {
1635+
const e = await render(<a title={'foo\rbar\r\nbaz\nqux\u0000'} />);
1636+
if (
1637+
render === serverRender ||
1638+
render === streamRender ||
1639+
render === clientRenderOnServerString
1640+
) {
1641+
// Everything becomes LF when parsed from server HTML.
1642+
// Null character in an attribute becomes the replacement character.
1643+
// Hydration also ends up with LF because we don't patch up attributes.
1644+
expect(e.title).toBe('foo\nbar\nbaz\nqux\uFFFD');
1645+
} else {
1646+
// Client rendering uses JS value with CR and null character.
1647+
expect(e.title).toBe('foo\rbar\r\nbaz\nqux\u0000');
1648+
}
1649+
},
1650+
);
1651+
});
1652+
15771653
describe('components that throw errors', function() {
15781654
itThrowsWhenRendering(
15791655
'a function returning undefined',

0 commit comments

Comments
 (0)