diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js
index c41170aa54e91..a856bd42edc1d 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js
@@ -87,17 +87,8 @@ describe('ReactDOMServerIntegration', () => {
{''}
,
);
- if (render === serverRender || render === streamRender) {
- // For plain server markup result we should have no text nodes if
- // they're all empty.
- expect(e.childNodes.length).toBe(0);
- expect(e.textContent).toBe('');
- } else {
- expect(e.childNodes.length).toBe(3);
- expectTextNode(e.childNodes[0], '');
- expectTextNode(e.childNodes[1], '');
- expectTextNode(e.childNodes[2], '');
- }
+ expect(e.childNodes.length).toBe(0);
+ expect(e.textContent).toBe('');
});
itRenders('a div with multiple whitespace children', async render => {
@@ -162,27 +153,14 @@ describe('ReactDOMServerIntegration', () => {
itRenders('a leading blank child with a text sibling', async render => {
const e = await render(
{''}foo
);
- if (render === serverRender || render === streamRender) {
- expect(e.childNodes.length).toBe(1);
- expectTextNode(e.childNodes[0], 'foo');
- } else {
- expect(e.childNodes.length).toBe(2);
- expectTextNode(e.childNodes[0], '');
- expectTextNode(e.childNodes[1], 'foo');
- }
+ expect(e.childNodes.length).toBe(1);
+ expectTextNode(e.childNodes[0], 'foo');
});
itRenders('a trailing blank child with a text sibling', async render => {
const e = await render(foo{''}
);
- // with Fiber, there are just two text nodes.
- if (render === serverRender || render === streamRender) {
- expect(e.childNodes.length).toBe(1);
- expectTextNode(e.childNodes[0], 'foo');
- } else {
- expect(e.childNodes.length).toBe(2);
- expectTextNode(e.childNodes[0], 'foo');
- expectTextNode(e.childNodes[1], '');
- }
+ expect(e.childNodes.length).toBe(1);
+ expectTextNode(e.childNodes[0], 'foo');
});
itRenders('an element with two text children', async render => {
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index b1a88868b3389..9bd4b0cf58e24 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -9,7 +9,7 @@
'use strict';
-let React;
+let React = require('react');
let ReactDOM;
let ReactDOMServer;
let Scheduler;
@@ -70,6 +70,17 @@ function dispatchMouseEvent(to, from) {
}
}
+class TestAppClass extends React.Component {
+ render() {
+ return (
+
+ <>{''}>
+ <>{'Hello'}>
+
+ );
+ }
+}
+
describe('ReactDOMServerPartialHydration', () => {
beforeEach(() => {
jest.resetModuleRegistry();
@@ -2738,4 +2749,49 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(span);
expect(ref.current.innerHTML).toBe('Hidden child');
});
+
+ function itHydratesWithoutMismatch(msg, App) {
+ it('hydrates without mismatch ' + msg, () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const finalHTML = ReactDOMServer.renderToString();
+ container.innerHTML = finalHTML;
+
+ ReactDOM.hydrateRoot(container, );
+ Scheduler.unstable_flushAll();
+ });
+ }
+
+ itHydratesWithoutMismatch('an empty string with neighbors', function App() {
+ return (
+
+
Test
+ {'' &&
Test
}
+ {'Test'}
+
+ );
+ });
+
+ itHydratesWithoutMismatch('an empty string', function App() {
+ return '';
+ });
+ itHydratesWithoutMismatch(
+ 'an empty string simple in fragment',
+ function App() {
+ return (
+ <>
+ {''}
+ {'sup'}
+ >
+ );
+ },
+ );
+ itHydratesWithoutMismatch(
+ 'an empty string simple in suspense',
+ function App() {
+ return {'' && false};
+ },
+ );
+
+ itHydratesWithoutMismatch('an empty string in class component', TestAppClass);
});
diff --git a/packages/react-dom/src/__tests__/ReactMultiChildText-test.js b/packages/react-dom/src/__tests__/ReactMultiChildText-test.js
index 2e99344289a7a..74c1d7bc3104e 100644
--- a/packages/react-dom/src/__tests__/ReactMultiChildText-test.js
+++ b/packages/react-dom/src/__tests__/ReactMultiChildText-test.js
@@ -53,6 +53,9 @@ const expectChildren = function(container, children) {
const child = children[i];
if (typeof child === 'string') {
+ if (child === '') {
+ continue;
+ }
textNode = outerNode.childNodes[mountIndex];
expect(textNode.nodeType).toBe(3);
expect(textNode.data).toBe(child);
@@ -83,7 +86,7 @@ describe('ReactMultiChildText', () => {
true, [],
0, '0',
1.2, '1.2',
- '', '',
+ '', [],
'foo', 'foo',
[], [],
@@ -93,7 +96,7 @@ describe('ReactMultiChildText', () => {
[true], [],
[0], ['0'],
[1.2], ['1.2'],
- [''], [''],
+ [''], [],
['foo'], ['foo'],
[], [],
diff --git a/packages/react-reconciler/src/ReactChildFiber.new.js b/packages/react-reconciler/src/ReactChildFiber.new.js
index 9071edc24f7a2..b49b8b605057d 100644
--- a/packages/react-reconciler/src/ReactChildFiber.new.js
+++ b/packages/react-reconciler/src/ReactChildFiber.new.js
@@ -488,7 +488,10 @@ function ChildReconciler(shouldTrackSideEffects) {
newChild: any,
lanes: Lanes,
): Fiber | null {
- if (typeof newChild === 'string' || typeof newChild === 'number') {
+ if (
+ (typeof newChild === 'string' && newChild !== '') ||
+ typeof newChild === 'number'
+ ) {
// Text nodes don't have keys. If the previous node is implicitly keyed
// we can continue to replace it without aborting even if it is not a text
// node.
@@ -564,7 +567,10 @@ function ChildReconciler(shouldTrackSideEffects) {
const key = oldFiber !== null ? oldFiber.key : null;
- if (typeof newChild === 'string' || typeof newChild === 'number') {
+ if (
+ (typeof newChild === 'string' && newChild !== '') ||
+ typeof newChild === 'number'
+ ) {
// Text nodes don't have keys. If the previous node is implicitly keyed
// we can continue to replace it without aborting even if it is not a text
// node.
@@ -626,7 +632,10 @@ function ChildReconciler(shouldTrackSideEffects) {
newChild: any,
lanes: Lanes,
): Fiber | null {
- if (typeof newChild === 'string' || typeof newChild === 'number') {
+ if (
+ (typeof newChild === 'string' && newChild !== '') ||
+ typeof newChild === 'number'
+ ) {
// Text nodes don't have keys, so we neither have to check the old nor
// new node for the key. If both are text nodes, they match.
const matchedFiber = existingChildren.get(newIdx) || null;
@@ -1299,7 +1308,10 @@ function ChildReconciler(shouldTrackSideEffects) {
throwOnInvalidObjectType(returnFiber, newChild);
}
- if (typeof newChild === 'string' || typeof newChild === 'number') {
+ if (
+ (typeof newChild === 'string' && newChild !== '') ||
+ typeof newChild === 'number'
+ ) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
diff --git a/packages/react-reconciler/src/ReactChildFiber.old.js b/packages/react-reconciler/src/ReactChildFiber.old.js
index 0128ca8f36f3d..ad62a3445fb22 100644
--- a/packages/react-reconciler/src/ReactChildFiber.old.js
+++ b/packages/react-reconciler/src/ReactChildFiber.old.js
@@ -488,7 +488,10 @@ function ChildReconciler(shouldTrackSideEffects) {
newChild: any,
lanes: Lanes,
): Fiber | null {
- if (typeof newChild === 'string' || typeof newChild === 'number') {
+ if (
+ (typeof newChild === 'string' && newChild !== '') ||
+ typeof newChild === 'number'
+ ) {
// Text nodes don't have keys. If the previous node is implicitly keyed
// we can continue to replace it without aborting even if it is not a text
// node.
@@ -564,7 +567,10 @@ function ChildReconciler(shouldTrackSideEffects) {
const key = oldFiber !== null ? oldFiber.key : null;
- if (typeof newChild === 'string' || typeof newChild === 'number') {
+ if (
+ (typeof newChild === 'string' && newChild !== '') ||
+ typeof newChild === 'number'
+ ) {
// Text nodes don't have keys. If the previous node is implicitly keyed
// we can continue to replace it without aborting even if it is not a text
// node.
@@ -626,7 +632,10 @@ function ChildReconciler(shouldTrackSideEffects) {
newChild: any,
lanes: Lanes,
): Fiber | null {
- if (typeof newChild === 'string' || typeof newChild === 'number') {
+ if (
+ (typeof newChild === 'string' && newChild !== '') ||
+ typeof newChild === 'number'
+ ) {
// Text nodes don't have keys, so we neither have to check the old nor
// new node for the key. If both are text nodes, they match.
const matchedFiber = existingChildren.get(newIdx) || null;
@@ -1299,7 +1308,10 @@ function ChildReconciler(shouldTrackSideEffects) {
throwOnInvalidObjectType(returnFiber, newChild);
}
- if (typeof newChild === 'string' || typeof newChild === 'number') {
+ if (
+ (typeof newChild === 'string' && newChild !== '') ||
+ typeof newChild === 'number'
+ ) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
index b507076e9e910..ccbed48fae2b4 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
@@ -673,7 +673,7 @@ describe('ReactIncrementalUpdates', () => {
root.render();
});
expect(Scheduler).toHaveYielded(['Committed: ']);
- expect(root).toMatchRenderedOutput('');
+ expect(root).toMatchRenderedOutput(null);
await act(async () => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {
@@ -734,7 +734,7 @@ describe('ReactIncrementalUpdates', () => {
root.render();
});
expect(Scheduler).toHaveYielded([]);
- expect(root).toMatchRenderedOutput('');
+ expect(root).toMatchRenderedOutput(null);
await act(async () => {
if (gate(flags => flags.enableSyncDefaultUpdates)) {