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
This commit is contained in:
Dan Abramov
2017-10-05 20:59:43 +01:00
committed by GitHub
parent 32ec797274
commit 44c32fc268
2 changed files with 115 additions and 5 deletions
@@ -76,16 +76,41 @@ if (__DEV__) {
validateUnknownProperties(type, props);
};
var warnForTextDifference = function(serverText: string, clientText: string) {
// HTML parsing normalizes CR and CRLF to LF.
// It also can turn \u0000 into \uFFFD inside attributes.
// https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
// If we have a mismatch, it might be caused by that.
// We will still patch up in this case but not fire the warning.
var NORMALIZE_NEWLINES_REGEX = /\r\n?/g;
var NORMALIZE_NULL_AND_REPLACEMENT_REGEX = /\u0000|\uFFFD/g;
var normalizeMarkupForTextOrAttribute = function(markup: mixed): string {
const markupString = typeof markup === 'string'
? markup
: '' + (markup: any);
return markupString
.replace(NORMALIZE_NEWLINES_REGEX, '\n')
.replace(NORMALIZE_NULL_AND_REPLACEMENT_REGEX, '');
};
var warnForTextDifference = function(
serverText: string,
clientText: string | number,
) {
if (didWarnInvalidHydration) {
return;
}
const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText);
const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText);
if (normalizedServerText === normalizedClientText) {
return;
}
didWarnInvalidHydration = true;
warning(
false,
'Text content did not match. Server: "%s" Client: "%s"',
serverText,
clientText,
normalizedServerText,
normalizedClientText,
);
};
@@ -97,13 +122,22 @@ if (__DEV__) {
if (didWarnInvalidHydration) {
return;
}
const normalizedClientValue = normalizeMarkupForTextOrAttribute(
clientValue,
);
const normalizedServerValue = normalizeMarkupForTextOrAttribute(
serverValue,
);
if (normalizedServerValue === normalizedClientValue) {
return;
}
didWarnInvalidHydration = true;
warning(
false,
'Prop `%s` did not match. Server: %s Client: %s',
propName,
JSON.stringify(serverValue),
JSON.stringify(clientValue),
JSON.stringify(normalizedServerValue),
JSON.stringify(normalizedClientValue),
);
};
@@ -1574,6 +1574,82 @@ describe('ReactDOMServerIntegration', () => {
});
});
describe('carriage return and null character', () => {
// HTML parsing normalizes CR and CRLF to LF.
// It also ignores null character.
// https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
// If we have a mismatch, it might be caused by that (and should not be reported).
// We won't be patching up in this case as that matches our past behavior.
itRenders(
'an element with one text child with special characters',
async render => {
const e = await render(<div>{'foo\rbar\r\nbaz\nqux\u0000'}</div>);
if (render === serverRender || render === streamRender) {
expect(e.childNodes.length).toBe(1);
// Everything becomes LF when parsed from server HTML.
// Null character is ignored.
expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar\nbaz\nqux');
} else {
expect(e.childNodes.length).toBe(1);
// Client rendering (or hydration) uses JS value with CR.
// Null character stays.
expectNode(
e.childNodes[0],
TEXT_NODE_TYPE,
'foo\rbar\r\nbaz\nqux\u0000',
);
}
},
);
itRenders(
'an element with two text children with special characters',
async render => {
const e = await render(<div>{'foo\rbar'}{'\r\nbaz\nqux\u0000'}</div>);
if (render === serverRender || render === streamRender) {
// We have three nodes because there is a comment between them.
expect(e.childNodes.length).toBe(3);
// Everything becomes LF when parsed from server HTML.
// Null character is ignored.
expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar');
expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\nbaz\nqux');
} else if (render === clientRenderOnServerString) {
// We have three nodes because there is a comment between them.
expect(e.childNodes.length).toBe(3);
// Hydration uses JS value with CR and null character.
expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar');
expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000');
} else {
expect(e.childNodes.length).toBe(2);
// Client rendering uses JS value with CR and null character.
expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar');
expectNode(e.childNodes[1], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000');
}
},
);
itRenders(
'an element with an attribute value with special characters',
async render => {
const e = await render(<a title={'foo\rbar\r\nbaz\nqux\u0000'} />);
if (
render === serverRender ||
render === streamRender ||
render === clientRenderOnServerString
) {
// Everything becomes LF when parsed from server HTML.
// Null character in an attribute becomes the replacement character.
// Hydration also ends up with LF because we don't patch up attributes.
expect(e.title).toBe('foo\nbar\nbaz\nqux\uFFFD');
} else {
// Client rendering uses JS value with CR and null character.
expect(e.title).toBe('foo\rbar\r\nbaz\nqux\u0000');
}
},
);
});
describe('components that throw errors', function() {
itThrowsWhenRendering(
'a function returning undefined',