diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js index c41170aa54e9..a856bd42edc1 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 f6bee806f3ac..d4f3886a1c4b 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(); @@ -2958,4 +2969,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 2e99344289a7..74c1d7bc3104 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 658b1f0e7b79..55246de7c59b 100644 --- a/packages/react-reconciler/src/ReactChildFiber.new.js +++ b/packages/react-reconciler/src/ReactChildFiber.new.js @@ -492,7 +492,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. @@ -568,7 +571,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. @@ -630,7 +636,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; @@ -1327,7 +1336,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 0ef3b301e95a..ae45c2eadcfa 100644 --- a/packages/react-reconciler/src/ReactChildFiber.old.js +++ b/packages/react-reconciler/src/ReactChildFiber.old.js @@ -492,7 +492,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. @@ -568,7 +571,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. @@ -630,7 +636,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; @@ -1327,7 +1336,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 b507076e9e91..ccbed48fae2b 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)) {