@samitouri / QOS-React-2 / commits / 44c32fc268

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

Dan Abramov committed Oct 5, 2017 at 20:59 UTC 44c32fc26891867f2a35fb3c9bbedbedd148ccbb
2 files changed +115 -5
src/renderers/dom/fiber/ReactDOMFiberComponent.js
+39 -5
@@ -76,16 +76,41 @@ if (__DEV__) {
76 validateUnknownProperties(type, props);
77 };
78
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 + ) {
100 if (didWarnInvalidHydration) {
101 return;
102 }
103 + const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText);
104 + const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText);
105 + if (normalizedServerText === normalizedClientText) {
106 + return;
107 + }
108 didWarnInvalidHydration = true;
109 warning(
110 false,
111 'Text content did not match. Server: "%s" Client: "%s"',
87 - serverText,
88 - clientText,
112 + normalizedServerText,
113 + normalizedClientText,
114 );
115 };
116
@@ -97,13 +122,22 @@ if (__DEV__) {
122 if (didWarnInvalidHydration) {
123 return;
124 }
125 + const normalizedClientValue = normalizeMarkupForTextOrAttribute(
126 + clientValue,
127 + );
128 + const normalizedServerValue = normalizeMarkupForTextOrAttribute(
129 + serverValue,
130 + );
131 + if (normalizedServerValue === normalizedClientValue) {
132 + return;
133 + }
134 didWarnInvalidHydration = true;
135 warning(
136 false,
137 'Prop `%s` did not match. Server: %s Client: %s',
138 propName,
105 - JSON.stringify(serverValue),
106 - JSON.stringify(clientValue),
139 + JSON.stringify(normalizedServerValue),
140 + JSON.stringify(normalizedClientValue),
141 );
142 };
143
src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
+76
@@ -1574,6 +1574,82 @@ describe('ReactDOMServerIntegration', () => {
1574 });
1575 });
1576
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 +
1653 describe('components that throw errors', function() {
1654 itThrowsWhenRendering(
1655 'a function returning undefined',