Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore CR/LF differences when warning about markup mismatch #11119

Merged
merged 15 commits into from
Oct 5, 2017
Merged
43 changes: 38 additions & 5 deletions src/renderers/dom/fiber/ReactDOMFiberComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,40 @@ if (__DEV__) {
validateUnknownProperties(type, props);
};

var warnForTextDifference = function(serverText: string, clientText: string) {
// HTML parsing normalizes CR and CRLF to LF.
// 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 won't be patching up in this case as that matches our past behavior.
var NORMALIZE_NEWLINES_REGEX = /\r\n?/g;
var NORMALIZE_NULL_REGEX = /\u0000/g;

var normalizeMarkupForTextOrAttribute = function(markup: mixed): string {
const markupString = typeof markup === 'string'
? markup
: '' + (markup: any);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not super proud but I couldn't get Flow to understand I'm just trying to cast to string.

Copy link
Contributor

@vjeux vjeux Oct 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String(markup)

return markupString
.replace(NORMALIZE_NEWLINES_REGEX, '\n')
.replace(NORMALIZE_NULL_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,
);
};

Expand All @@ -97,13 +121,22 @@ if (__DEV__) {
if (didWarnInvalidHydration) {
return;
}
const normalizedClientValue = normalizeMarkupForTextOrAttribute(
clientValue,
);
const normalizedServerValue = normalizeMarkupForTextOrAttribute(
clientValue,
);
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),
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,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',
Expand Down