@samitouri / QOS-React / commits / be4533af7d

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