@samitouri / QOS-React / commits / c1220ebdde

treat empty string as null (#22807)

salazarm committed Nov 23, 2021 at 18:40 UTC c1220ebdde506de91c8b9693b5cb67ac710c8c89
6 files changed +102 -41
packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js
+6 -28
@@ -87,17 +87,8 @@ describe('ReactDOMServerIntegration', () => {
87 {''}
88 </div>,
89 );
90 - if (render === serverRender || render === streamRender) {
91 - // For plain server markup result we should have no text nodes if
92 - // they're all empty.
93 - expect(e.childNodes.length).toBe(0);
94 - expect(e.textContent).toBe('');
95 - } else {
96 - expect(e.childNodes.length).toBe(3);
97 - expectTextNode(e.childNodes[0], '');
98 - expectTextNode(e.childNodes[1], '');
99 - expectTextNode(e.childNodes[2], '');
100 - }
90 + expect(e.childNodes.length).toBe(0);
91 + expect(e.textContent).toBe('');
92 });
93
94 itRenders('a div with multiple whitespace children', async render => {
@@ -162,27 +153,14 @@ describe('ReactDOMServerIntegration', () => {
153
154 itRenders('a leading blank child with a text sibling', async render => {
155 const e = await render(<div>{''}foo</div>);
165 - if (render === serverRender || render === streamRender) {
166 - expect(e.childNodes.length).toBe(1);
167 - expectTextNode(e.childNodes[0], 'foo');
168 - } else {
169 - expect(e.childNodes.length).toBe(2);
170 - expectTextNode(e.childNodes[0], '');
171 - expectTextNode(e.childNodes[1], 'foo');
172 - }
156 + expect(e.childNodes.length).toBe(1);
157 + expectTextNode(e.childNodes[0], 'foo');
158 });
159
160 itRenders('a trailing blank child with a text sibling', async render => {
161 const e = await render(<div>foo{''}</div>);
177 - // with Fiber, there are just two text nodes.
178 - if (render === serverRender || render === streamRender) {
179 - expect(e.childNodes.length).toBe(1);
180 - expectTextNode(e.childNodes[0], 'foo');
181 - } else {
182 - expect(e.childNodes.length).toBe(2);
183 - expectTextNode(e.childNodes[0], 'foo');
184 - expectTextNode(e.childNodes[1], '');
185 - }
162 + expect(e.childNodes.length).toBe(1);
163 + expectTextNode(e.childNodes[0], 'foo');
164 });
165
166 itRenders('an element with two text children', async render => {
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+57 -1
@@ -9,7 +9,7 @@
9
10 'use strict';
11
12 -let React;
12 +let React = require('react');
13 let ReactDOM;
14 let ReactDOMServer;
15 let Scheduler;
@@ -70,6 +70,17 @@ function dispatchMouseEvent(to, from) {
70 }
71 }
72
73 +class TestAppClass extends React.Component {
74 + render() {
75 + return (
76 + <div>
77 + <>{''}</>
78 + <>{'Hello'}</>
79 + </div>
80 + );
81 + }
82 +}
83 +
84 describe('ReactDOMServerPartialHydration', () => {
85 beforeEach(() => {
86 jest.resetModuleRegistry();
@@ -2958,4 +2969,49 @@ describe('ReactDOMServerPartialHydration', () => {
2969 expect(ref.current).toBe(span);
2970 expect(ref.current.innerHTML).toBe('Hidden child');
2971 });
2972 +
2973 + function itHydratesWithoutMismatch(msg, App) {
2974 + it('hydrates without mismatch ' + msg, () => {
2975 + const container = document.createElement('div');
2976 + document.body.appendChild(container);
2977 + const finalHTML = ReactDOMServer.renderToString(<App />);
2978 + container.innerHTML = finalHTML;
2979 +
2980 + ReactDOM.hydrateRoot(container, <App />);
2981 + Scheduler.unstable_flushAll();
2982 + });
2983 + }
2984 +
2985 + itHydratesWithoutMismatch('an empty string with neighbors', function App() {
2986 + return (
2987 + <div>
2988 + <div id="test">Test</div>
2989 + {'' && <div>Test</div>}
2990 + {'Test'}
2991 + </div>
2992 + );
2993 + });
2994 +
2995 + itHydratesWithoutMismatch('an empty string', function App() {
2996 + return '';
2997 + });
2998 + itHydratesWithoutMismatch(
2999 + 'an empty string simple in fragment',
3000 + function App() {
3001 + return (
3002 + <>
3003 + {''}
3004 + {'sup'}
3005 + </>
3006 + );
3007 + },
3008 + );
3009 + itHydratesWithoutMismatch(
3010 + 'an empty string simple in suspense',
3011 + function App() {
3012 + return <Suspense>{'' && false}</Suspense>;
3013 + },
3014 + );
3015 +
3016 + itHydratesWithoutMismatch('an empty string in class component', TestAppClass);
3017 });
packages/react-dom/src/__tests__/ReactMultiChildText-test.js
+5 -2
@@ -53,6 +53,9 @@ const expectChildren = function(container, children) {
53 const child = children[i];
54
55 if (typeof child === 'string') {
56 + if (child === '') {
57 + continue;
58 + }
59 textNode = outerNode.childNodes[mountIndex];
60 expect(textNode.nodeType).toBe(3);
61 expect(textNode.data).toBe(child);
@@ -83,7 +86,7 @@ describe('ReactMultiChildText', () => {
86 true, [],
87 0, '0',
88 1.2, '1.2',
86 - '', '',
89 + '', [],
90 'foo', 'foo',
91
92 [], [],
@@ -93,7 +96,7 @@ describe('ReactMultiChildText', () => {
96 [true], [],
97 [0], ['0'],
98 [1.2], ['1.2'],
96 - [''], [''],
99 + [''], [],
100 ['foo'], ['foo'],
101 [<div />], [<div />],
102
packages/react-reconciler/src/ReactChildFiber.new.js
+16 -4
@@ -492,7 +492,10 @@ function ChildReconciler(shouldTrackSideEffects) {
492 newChild: any,
493 lanes: Lanes,
494 ): Fiber | null {
495 - if (typeof newChild === 'string' || typeof newChild === 'number') {
495 + if (
496 + (typeof newChild === 'string' && newChild !== '') ||
497 + typeof newChild === 'number'
498 + ) {
499 // Text nodes don't have keys. If the previous node is implicitly keyed
500 // we can continue to replace it without aborting even if it is not a text
501 // node.
@@ -568,7 +571,10 @@ function ChildReconciler(shouldTrackSideEffects) {
571
572 const key = oldFiber !== null ? oldFiber.key : null;
573
571 - if (typeof newChild === 'string' || typeof newChild === 'number') {
574 + if (
575 + (typeof newChild === 'string' && newChild !== '') ||
576 + typeof newChild === 'number'
577 + ) {
578 // Text nodes don't have keys. If the previous node is implicitly keyed
579 // we can continue to replace it without aborting even if it is not a text
580 // node.
@@ -630,7 +636,10 @@ function ChildReconciler(shouldTrackSideEffects) {
636 newChild: any,
637 lanes: Lanes,
638 ): Fiber | null {
633 - if (typeof newChild === 'string' || typeof newChild === 'number') {
639 + if (
640 + (typeof newChild === 'string' && newChild !== '') ||
641 + typeof newChild === 'number'
642 + ) {
643 // Text nodes don't have keys, so we neither have to check the old nor
644 // new node for the key. If both are text nodes, they match.
645 const matchedFiber = existingChildren.get(newIdx) || null;
@@ -1327,7 +1336,10 @@ function ChildReconciler(shouldTrackSideEffects) {
1336 throwOnInvalidObjectType(returnFiber, newChild);
1337 }
1338
1330 - if (typeof newChild === 'string' || typeof newChild === 'number') {
1339 + if (
1340 + (typeof newChild === 'string' && newChild !== '') ||
1341 + typeof newChild === 'number'
1342 + ) {
1343 return placeSingleChild(
1344 reconcileSingleTextNode(
1345 returnFiber,
packages/react-reconciler/src/ReactChildFiber.old.js
+16 -4
@@ -492,7 +492,10 @@ function ChildReconciler(shouldTrackSideEffects) {
492 newChild: any,
493 lanes: Lanes,
494 ): Fiber | null {
495 - if (typeof newChild === 'string' || typeof newChild === 'number') {
495 + if (
496 + (typeof newChild === 'string' && newChild !== '') ||
497 + typeof newChild === 'number'
498 + ) {
499 // Text nodes don't have keys. If the previous node is implicitly keyed
500 // we can continue to replace it without aborting even if it is not a text
501 // node.
@@ -568,7 +571,10 @@ function ChildReconciler(shouldTrackSideEffects) {
571
572 const key = oldFiber !== null ? oldFiber.key : null;
573
571 - if (typeof newChild === 'string' || typeof newChild === 'number') {
574 + if (
575 + (typeof newChild === 'string' && newChild !== '') ||
576 + typeof newChild === 'number'
577 + ) {
578 // Text nodes don't have keys. If the previous node is implicitly keyed
579 // we can continue to replace it without aborting even if it is not a text
580 // node.
@@ -630,7 +636,10 @@ function ChildReconciler(shouldTrackSideEffects) {
636 newChild: any,
637 lanes: Lanes,
638 ): Fiber | null {
633 - if (typeof newChild === 'string' || typeof newChild === 'number') {
639 + if (
640 + (typeof newChild === 'string' && newChild !== '') ||
641 + typeof newChild === 'number'
642 + ) {
643 // Text nodes don't have keys, so we neither have to check the old nor
644 // new node for the key. If both are text nodes, they match.
645 const matchedFiber = existingChildren.get(newIdx) || null;
@@ -1327,7 +1336,10 @@ function ChildReconciler(shouldTrackSideEffects) {
1336 throwOnInvalidObjectType(returnFiber, newChild);
1337 }
1338
1330 - if (typeof newChild === 'string' || typeof newChild === 'number') {
1339 + if (
1340 + (typeof newChild === 'string' && newChild !== '') ||
1341 + typeof newChild === 'number'
1342 + ) {
1343 return placeSingleChild(
1344 reconcileSingleTextNode(
1345 returnFiber,
packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
+2 -2
@@ -673,7 +673,7 @@ describe('ReactIncrementalUpdates', () => {
673 root.render(<App />);
674 });
675 expect(Scheduler).toHaveYielded(['Committed: ']);
676 - expect(root).toMatchRenderedOutput('');
676 + expect(root).toMatchRenderedOutput(null);
677
678 await act(async () => {
679 if (gate(flags => flags.enableSyncDefaultUpdates)) {
@@ -734,7 +734,7 @@ describe('ReactIncrementalUpdates', () => {
734 root.render(<App />);
735 });
736 expect(Scheduler).toHaveYielded([]);
737 - expect(root).toMatchRenderedOutput('');
737 + expect(root).toMatchRenderedOutput(null);
738
739 await act(async () => {
740 if (gate(flags => flags.enableSyncDefaultUpdates)) {