diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js index dc0631d1d0e08..39f9cf4c2ad4a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js @@ -485,16 +485,97 @@ describe('ReactDOMServerIntegration', () => { expect(e.tagName).toBe('BUTTON'); }); - itRenders('a div with dangerouslySetInnerHTML', async render => { - const e = await render( -
"}} />, - ); + itRenders('a div with dangerouslySetInnerHTML number', async render => { + // Put dangerouslySetInnerHTML one level deeper because otherwise + // hydrating from a bad markup would cause a mismatch (since we don't + // patch dangerouslySetInnerHTML as text content). + const e = (await render( +
+ +
, + )).firstChild; + expect(e.childNodes.length).toBe(1); + expect(e.firstChild.nodeType).toBe(TEXT_NODE_TYPE); + expect(e.textContent).toBe('0'); + }); + + itRenders('a div with dangerouslySetInnerHTML boolean', async render => { + // Put dangerouslySetInnerHTML one level deeper because otherwise + // hydrating from a bad markup would cause a mismatch (since we don't + // patch dangerouslySetInnerHTML as text content). + const e = (await render( +
+ +
, + )).firstChild; + expect(e.childNodes.length).toBe(1); + expect(e.firstChild.nodeType).toBe(TEXT_NODE_TYPE); + expect(e.firstChild.data).toBe('false'); + }); + + itRenders( + 'a div with dangerouslySetInnerHTML text string', + async render => { + // Put dangerouslySetInnerHTML one level deeper because otherwise + // hydrating from a bad markup would cause a mismatch (since we don't + // patch dangerouslySetInnerHTML as text content). + const e = (await render( +
+ +
, + )).firstChild; + expect(e.childNodes.length).toBe(1); + expect(e.firstChild.nodeType).toBe(TEXT_NODE_TYPE); + expect(e.textContent).toBe('hello'); + }, + ); + + itRenders( + 'a div with dangerouslySetInnerHTML element string', + async render => { + const e = await render( +
"}} />, + ); + expect(e.childNodes.length).toBe(1); + expect(e.firstChild.tagName).toBe('SPAN'); + expect(e.firstChild.getAttribute('id')).toBe('child'); + expect(e.firstChild.childNodes.length).toBe(0); + }, + ); + + itRenders('a div with dangerouslySetInnerHTML object', async render => { + const obj = { + toString() { + return ""; + }, + }; + const e = await render(
); expect(e.childNodes.length).toBe(1); expect(e.firstChild.tagName).toBe('SPAN'); expect(e.firstChild.getAttribute('id')).toBe('child'); expect(e.firstChild.childNodes.length).toBe(0); }); + itRenders( + 'a div with dangerouslySetInnerHTML set to null', + async render => { + const e = await render( +
, + ); + expect(e.childNodes.length).toBe(0); + }, + ); + + itRenders( + 'a div with dangerouslySetInnerHTML set to undefined', + async render => { + const e = await render( +
, + ); + expect(e.childNodes.length).toBe(0); + }, + ); + describe('newline-eating elements', function() { itRenders( 'a newline-eating tag with content not starting with \\n', diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationReconnecting-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationReconnecting-test.js index c10450c436f1d..79e90e6555fa8 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationReconnecting-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationReconnecting-test.js @@ -412,6 +412,40 @@ describe('ReactDOMServerIntegration', () => {
"}} />, )); + it('should error reconnecting a div with different text dangerouslySetInnerHTML', () => + expectMarkupMismatch( +
, +
, + )); + + it('should error reconnecting a div with different number dangerouslySetInnerHTML', () => + expectMarkupMismatch( +
, +
, + )); + + it('should error reconnecting a div with different object dangerouslySetInnerHTML', () => + expectMarkupMismatch( +
, +
, + )); + it('can explicitly ignore reconnecting a div with different dangerouslySetInnerHTML', () => expectMarkupMatch(
"}} />, diff --git a/packages/react-dom/src/client/ReactDOMFiberComponent.js b/packages/react-dom/src/client/ReactDOMFiberComponent.js index 3928541fda88c..1d94ab33c91d9 100644 --- a/packages/react-dom/src/client/ReactDOMFiberComponent.js +++ b/packages/react-dom/src/client/ReactDOMFiberComponent.js @@ -987,9 +987,12 @@ export function diffHydratedProperties( ) { // Noop } else if (propKey === DANGEROUSLY_SET_INNER_HTML) { - const rawHtml = nextProp ? nextProp[HTML] || '' : ''; const serverHTML = domElement.innerHTML; - const expectedHTML = normalizeHTML(domElement, rawHtml); + const nextHtml = nextProp ? nextProp[HTML] : undefined; + const expectedHTML = normalizeHTML( + domElement, + nextHtml != null ? nextHtml : '', + ); if (expectedHTML !== serverHTML) { warnForPropDifference(propKey, serverHTML, expectedHTML); } diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 41d7253f4b1fc..0b3d930a4b673 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -249,7 +249,7 @@ export function shouldSetTextContent(type: string, props: Props): boolean { typeof props.children === 'number' || (typeof props.dangerouslySetInnerHTML === 'object' && props.dangerouslySetInnerHTML !== null && - typeof props.dangerouslySetInnerHTML.__html === 'string') + props.dangerouslySetInnerHTML.__html != null) ); }