Fix hydration of non-string dangerousSetInnerHTML.__html (#13353)
* Consistently handle non-string dangerousSetInnerHTML.__html in SSR * Add another test
Dan Abramov committed
Aug 9, 2018 at 18:05 UTC
be4533af7d29f020499d3c01931e55ef8666a419
4 files changed
+125
-7
packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js
+85
-4
@@ -485,16 +485,97 @@ describe('ReactDOMServerIntegration', () => {
485
expect(e.tagName).toBe('BUTTON');
486
});
487
488
- itRenders('a div with dangerouslySetInnerHTML', async render => {
489
- const e = await render(
490
- <div dangerouslySetInnerHTML={{__html: "<span id='child'/>"}} />,
491
- );
488
+ itRenders('a div with dangerouslySetInnerHTML number', async render => {
489
+ // Put dangerouslySetInnerHTML one level deeper because otherwise
490
+ // hydrating from a bad markup would cause a mismatch (since we don't
491
+ // patch dangerouslySetInnerHTML as text content).
492
+ const e = (await render(
493
+ <div>
494
+ <span dangerouslySetInnerHTML={{__html: 0}} />
495
+ </div>,
496
+ )).firstChild;
497
+ expect(e.childNodes.length).toBe(1);
498
+ expect(e.firstChild.nodeType).toBe(TEXT_NODE_TYPE);
499
+ expect(e.textContent).toBe('0');
500
+ });
501
+
502
+ itRenders('a div with dangerouslySetInnerHTML boolean', async render => {
503
+ // Put dangerouslySetInnerHTML one level deeper because otherwise
504
+ // hydrating from a bad markup would cause a mismatch (since we don't
505
+ // patch dangerouslySetInnerHTML as text content).
506
+ const e = (await render(
507
+ <div>
508
+ <span dangerouslySetInnerHTML={{__html: false}} />
509
+ </div>,
510
+ )).firstChild;
511
+ expect(e.childNodes.length).toBe(1);
512
+ expect(e.firstChild.nodeType).toBe(TEXT_NODE_TYPE);
513
+ expect(e.firstChild.data).toBe('false');
514
+ });
515
+
516
+ itRenders(
517
+ 'a div with dangerouslySetInnerHTML text string',
518
+ async render => {
519
+ // Put dangerouslySetInnerHTML one level deeper because otherwise
520
+ // hydrating from a bad markup would cause a mismatch (since we don't
521
+ // patch dangerouslySetInnerHTML as text content).
522
+ const e = (await render(
523
+ <div>
524
+ <span dangerouslySetInnerHTML={{__html: 'hello'}} />
525
+ </div>,
526
+ )).firstChild;
527
+ expect(e.childNodes.length).toBe(1);
528
+ expect(e.firstChild.nodeType).toBe(TEXT_NODE_TYPE);
529
+ expect(e.textContent).toBe('hello');
530
+ },
531
+ );
532
+
533
+ itRenders(
534
+ 'a div with dangerouslySetInnerHTML element string',
535
+ async render => {
536
+ const e = await render(
537
+ <div dangerouslySetInnerHTML={{__html: "<span id='child'/>"}} />,
538
+ );
539
+ expect(e.childNodes.length).toBe(1);
540
+ expect(e.firstChild.tagName).toBe('SPAN');
541
+ expect(e.firstChild.getAttribute('id')).toBe('child');
542
+ expect(e.firstChild.childNodes.length).toBe(0);
543
+ },
544
+ );
545
+
546
+ itRenders('a div with dangerouslySetInnerHTML object', async render => {
547
+ const obj = {
548
+ toString() {
549
+ return "<span id='child'/>";
550
+ },
551
+ };
552
+ const e = await render(<div dangerouslySetInnerHTML={{__html: obj}} />);
553
expect(e.childNodes.length).toBe(1);
554
expect(e.firstChild.tagName).toBe('SPAN');
555
expect(e.firstChild.getAttribute('id')).toBe('child');
556
expect(e.firstChild.childNodes.length).toBe(0);
557
});
558
559
+ itRenders(
560
+ 'a div with dangerouslySetInnerHTML set to null',
561
+ async render => {
562
+ const e = await render(
563
+ <div dangerouslySetInnerHTML={{__html: null}} />,
564
+ );
565
+ expect(e.childNodes.length).toBe(0);
566
+ },
567
+ );
568
+
569
+ itRenders(
570
+ 'a div with dangerouslySetInnerHTML set to undefined',
571
+ async render => {
572
+ const e = await render(
573
+ <div dangerouslySetInnerHTML={{__html: undefined}} />,
574
+ );
575
+ expect(e.childNodes.length).toBe(0);
576
+ },
577
+ );
578
+
579
describe('newline-eating elements', function() {
580
itRenders(
581
'a newline-eating tag with content not starting with \\n',
packages/react-dom/src/__tests__/ReactDOMServerIntegrationReconnecting-test.js
+34
@@ -412,6 +412,40 @@ describe('ReactDOMServerIntegration', () => {
412
<div dangerouslySetInnerHTML={{__html: "<span id='child2'/>"}} />,
413
));
414
415
+ it('should error reconnecting a div with different text dangerouslySetInnerHTML', () =>
416
+ expectMarkupMismatch(
417
+ <div dangerouslySetInnerHTML={{__html: 'foo'}} />,
418
+ <div dangerouslySetInnerHTML={{__html: 'bar'}} />,
419
+ ));
420
+
421
+ it('should error reconnecting a div with different number dangerouslySetInnerHTML', () =>
422
+ expectMarkupMismatch(
423
+ <div dangerouslySetInnerHTML={{__html: 10}} />,
424
+ <div dangerouslySetInnerHTML={{__html: 20}} />,
425
+ ));
426
+
427
+ it('should error reconnecting a div with different object dangerouslySetInnerHTML', () =>
428
+ expectMarkupMismatch(
429
+ <div
430
+ dangerouslySetInnerHTML={{
431
+ __html: {
432
+ toString() {
433
+ return 'hi';
434
+ },
435
+ },
436
+ }}
437
+ />,
438
+ <div
439
+ dangerouslySetInnerHTML={{
440
+ __html: {
441
+ toString() {
442
+ return 'bye';
443
+ },
444
+ },
445
+ }}
446
+ />,
447
+ ));
448
+
449
it('can explicitly ignore reconnecting a div with different dangerouslySetInnerHTML', () =>
450
expectMarkupMatch(
451
<div dangerouslySetInnerHTML={{__html: "<span id='child1'/>"}} />,
packages/react-dom/src/client/ReactDOMFiberComponent.js
+5
-2
@@ -987,9 +987,12 @@ export function diffHydratedProperties(
987
) {
988
// Noop
989
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
990
- const rawHtml = nextProp ? nextProp[HTML] || '' : '';
990
const serverHTML = domElement.innerHTML;
992
- const expectedHTML = normalizeHTML(domElement, rawHtml);
991
+ const nextHtml = nextProp ? nextProp[HTML] : undefined;
992
+ const expectedHTML = normalizeHTML(
993
+ domElement,
994
+ nextHtml != null ? nextHtml : '',
995
+ );
996
if (expectedHTML !== serverHTML) {
997
warnForPropDifference(propKey, serverHTML, expectedHTML);
998
}
packages/react-dom/src/client/ReactDOMHostConfig.js
+1
-1
@@ -249,7 +249,7 @@ export function shouldSetTextContent(type: string, props: Props): boolean {
249
typeof props.children === 'number' ||
250
(typeof props.dangerouslySetInnerHTML === 'object' &&
251
props.dangerouslySetInnerHTML !== null &&
252
- typeof props.dangerouslySetInnerHTML.__html === 'string')
252
+ props.dangerouslySetInnerHTML.__html != null)
253
);
254
}
255