mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user