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)) {