From 1b21177be4cc8bbda193a7b3593b8b5d128672ce Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 4 Oct 2017 19:49:02 -0700 Subject: [PATCH 1/6] Ignore SSR warning using explicit suppressHydrationWarning option This lets you ignore the warning on a single element and its direct child content. This is useful for simple fields that you're expecting to fail such as time stamps. Note that this still won't patch up such content so it'll remain inconsistent. It's also not suitable for nested complex content that may change. --- .../dom/fiber/ReactDOMFiberComponent.js | 29 ++++-- src/renderers/dom/shared/DOMProperty.js | 1 + .../__tests__/ReactDOMComponent-test.js | 8 ++ .../ReactDOMServerIntegration-test.js | 97 +++++++++++++++++++ .../dom/shared/hooks/possibleStandardNames.js | 1 + .../shared/server/ReactPartialRenderer.js | 1 + 6 files changed, 130 insertions(+), 7 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberComponent.js b/src/renderers/dom/fiber/ReactDOMFiberComponent.js index 8cb1f608ba574..1058ada33e926 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberComponent.js +++ b/src/renderers/dom/fiber/ReactDOMFiberComponent.js @@ -53,6 +53,7 @@ var registrationNameModules = EventPluginRegistry.registrationNameModules; var DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML'; var SUPPRESS_CONTENT_EDITABLE_WARNING = 'suppressContentEditableWarning'; +var SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning'; var CHILDREN = 'children'; var STYLE = 'style'; var HTML = '__html'; @@ -273,7 +274,10 @@ function setInitialDOMProperties( } else if (typeof nextProp === 'number') { setTextContent(domElement, '' + nextProp); } - } else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING) { + } else if ( + propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || + propKey === SUPPRESS_HYDRATION_WARNING + ) { // Noop } else if (registrationNameModules.hasOwnProperty(propKey)) { if (nextProp != null) { @@ -664,7 +668,10 @@ var ReactDOMFiberComponent = { propKey === CHILDREN ) { // Noop. This is handled by the clear text mechanism. - } else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING) { + } else if ( + propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || + propKey === SUPPRESS_HYDRATION_WARNING + ) { // Noop } else if (registrationNameModules.hasOwnProperty(propKey)) { // This is a special case. If any listener updates we need to ensure @@ -750,7 +757,10 @@ var ReactDOMFiberComponent = { ) { (updatePayload = updatePayload || []).push(propKey, '' + nextProp); } - } else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING) { + } else if ( + propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || + propKey === SUPPRESS_HYDRATION_WARNING + ) { // Noop } else if (registrationNameModules.hasOwnProperty(propKey)) { if (nextProp != null) { @@ -828,6 +838,8 @@ var ReactDOMFiberComponent = { rootContainerElement: Element | Document, ): null | Array { if (__DEV__) { + var suppressHydrationWarning = + rawProps[SUPPRESS_HYDRATION_WARNING] === true; var isCustomComponentTag = isCustomComponent(tag, rawProps); validatePropertiesInDevelopment(tag, rawProps); if (isCustomComponentTag && !didWarnShadyDOM && domElement.shadyRoot) { @@ -986,14 +998,14 @@ var ReactDOMFiberComponent = { // TODO: Should we use domElement.firstChild.nodeValue to compare? if (typeof nextProp === 'string') { if (domElement.textContent !== nextProp) { - if (__DEV__) { + if (__DEV__ && !suppressHydrationWarning) { warnForTextDifference(domElement.textContent, nextProp); } updatePayload = [CHILDREN, nextProp]; } } else if (typeof nextProp === 'number') { if (domElement.textContent !== '' + nextProp) { - if (__DEV__) { + if (__DEV__ && !suppressHydrationWarning) { warnForTextDifference(domElement.textContent, nextProp); } updatePayload = [CHILDREN, '' + nextProp]; @@ -1010,8 +1022,11 @@ var ReactDOMFiberComponent = { // Validate that the properties correspond to their expected values. var serverValue; var propertyInfo; - if ( + if (suppressHydrationWarning) { + // Don't bother comparing. We're ignoring all these warnings. + } else if ( propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || + propKey === SUPPRESS_HYDRATION_WARNING || // Controlled attributes are not validated // TODO: Only ignore them on controlled tags. propKey === 'value' || @@ -1085,7 +1100,7 @@ var ReactDOMFiberComponent = { if (__DEV__) { // $FlowFixMe - Should be inferred as not undefined. - if (extraAttributeNames.size > 0) { + if (extraAttributeNames.size > 0 && !suppressHydrationWarning) { // $FlowFixMe - Should be inferred as not undefined. warnForExtraAttributes(extraAttributeNames); } diff --git a/src/renderers/dom/shared/DOMProperty.js b/src/renderers/dom/shared/DOMProperty.js index 6ca4c3cbc4914..197b57971baad 100644 --- a/src/renderers/dom/shared/DOMProperty.js +++ b/src/renderers/dom/shared/DOMProperty.js @@ -21,6 +21,7 @@ var RESERVED_PROPS = { defaultChecked: true, innerHTML: true, suppressContentEditableWarning: true, + suppressHydrationWarning: true, style: true, }; diff --git a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js index 8c8c9ea1e41ef..b140a31301993 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js @@ -318,6 +318,7 @@ describe('ReactDOMComponent', () => { , container, ); @@ -325,11 +326,15 @@ describe('ReactDOMComponent', () => { expect( container.firstChild.hasAttribute('suppressContentEditableWarning'), ).toBe(false); + expect( + container.firstChild.hasAttribute('suppressHydrationWarning'), + ).toBe(false); ReactDOM.render( , container, ); @@ -337,6 +342,9 @@ describe('ReactDOMComponent', () => { expect( container.firstChild.hasAttribute('suppressContentEditableWarning'), ).toBe(false); + expect( + container.firstChild.hasAttribute('suppressHydrationWarning'), + ).toBe(false); }); it('should skip dangerouslySetInnerHTML on web components', () => { diff --git a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js index faa22ff76ccfd..f4949a77ce596 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js @@ -725,6 +725,16 @@ describe('ReactDOMServerIntegration', () => { ); expect(e.getAttribute('dangerouslySetInnerHTML')).toBe(null); }); + + itRenders('no suppressContentEditableWarning attribute', async render => { + const e = await render(
); + expect(e.getAttribute('suppressContentEditableWarning')).toBe(null); + }); + + itRenders('no suppressHydrationWarning attribute', async render => { + const e = await render(); + expect(e.getAttribute('suppressHydrationWarning')).toBe(null); + }); }); describe('inline styles', function() { @@ -2623,6 +2633,36 @@ describe('ReactDOMServerIntegration', () => { it('should error reconnecting different attribute values', () => expectMarkupMismatch(
,
)); + + it('can explicitly ignore errors reconnecting different element types of children', () => + expectMarkupMatch( +
, +
, + )); + + it('can explicitly ignore errors reconnecting missing attributes', () => + expectMarkupMatch( +
, +
, + )); + + it('can explicitly ignore errors reconnecting added attributes', () => + expectMarkupMatch( +
, +
, + )); + + it('can explicitly ignore errors reconnecting different attribute values', () => + expectMarkupMatch( +
, +
, + )); + + it('can not deeply ignore errors reconnecting different attribute values', () => + expectMarkupMismatch( +
, +
, + )); }); describe('inline styles', function() { @@ -2661,6 +2701,18 @@ describe('ReactDOMServerIntegration', () => {
,
, )); + + it('can explicitly ignore errors reconnecting added style values', () => + expectMarkupMatch( +
, +
, + )); + + it('can explicitly ignore reconnecting different style values', () => + expectMarkupMatch( +
, +
, + )); }); describe('text nodes', function() { @@ -2681,6 +2733,18 @@ describe('ReactDOMServerIntegration', () => {
{'Text1'}{'Text2'}
,
{'Text1'}{'Text3'}
, )); + + it('can explicitly ignore reconnecting different text', () => + expectMarkupMatch( +
Text
, +
Other Text
, + )); + + it('can explicitly ignore reconnecting different text in two code blocks', () => + expectMarkupMatch( +
{'Text1'}{'Text2'}
, +
{'Text1'}{'Text3'}
, + )); }); describe('element trees and children', function() { @@ -2731,6 +2795,30 @@ describe('ReactDOMServerIntegration', () => { it('can distinguish an empty component from an empty text component', () => expectMarkupMatch(
,
{''}
)); + + it('can explicitly ignore reconnecting more children', () => + expectMarkupMatch( +
, +
, + )); + + it('can explicitly ignore reconnecting fewer children', () => + expectMarkupMatch( +
, +
, + )); + + it('can explicitly ignore reconnecting reordered children', () => + expectMarkupMatch( +
, +
, + )); + + it('can not deeply ignore reconnecting reordered children', () => + expectMarkupMismatch( +
, +
, + )); }); // Markup Mismatches: misc @@ -2739,5 +2827,14 @@ describe('ReactDOMServerIntegration', () => {
"}} />,
"}} />, )); + + it('can explicitly ignore reconnecting a div with different dangerouslySetInnerHTML', () => + expectMarkupMatch( +
"}} />, +
"}} + suppressHydrationWarning={true} + />, + )); }); }); diff --git a/src/renderers/dom/shared/hooks/possibleStandardNames.js b/src/renderers/dom/shared/hooks/possibleStandardNames.js index 236de2e0e9aef..0349d4ce4e0b7 100644 --- a/src/renderers/dom/shared/hooks/possibleStandardNames.js +++ b/src/renderers/dom/shared/hooks/possibleStandardNames.js @@ -401,6 +401,7 @@ var possibleStandardNames = { strokeopacity: 'strokeOpacity', 'stroke-opacity': 'strokeOpacity', suppresscontenteditablewarning: 'suppressContentEditableWarning', + suppresshydrationwarning: 'suppressHydrationWarning', surfacescale: 'surfaceScale', systemlanguage: 'systemLanguage', tablevalues: 'tableValues', diff --git a/src/renderers/shared/server/ReactPartialRenderer.js b/src/renderers/shared/server/ReactPartialRenderer.js index facf1c2bc88b9..9f7e41ffd1f45 100644 --- a/src/renderers/shared/server/ReactPartialRenderer.js +++ b/src/renderers/shared/server/ReactPartialRenderer.js @@ -251,6 +251,7 @@ var RESERVED_PROPS = { children: null, dangerouslySetInnerHTML: null, suppressContentEditableWarning: null, + suppressHydrationWarning: null, }; function createOpenTagMarkup( From 3489df31bf381ec82b6d1b7d320c9e4e9fa52fee Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 4 Oct 2017 19:46:06 -0700 Subject: [PATCH 2/6] Pass parent type and props to insert/delete hydration warning hooks For this to work, we need to split the API into a container and normal version. Since the root doesn't have a type nor props. --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 38 +++++++++- .../fiber/ReactFiberHydrationContext.js | 76 ++++++++++++++----- .../shared/fiber/ReactFiberReconciler.js | 28 ++++++- 3 files changed, 118 insertions(+), 24 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 0e0559d47a1d7..ea56474247cec 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -523,8 +523,21 @@ var DOMRenderer = ReactFiberReconciler({ return diffHydratedText(textInstance, text); }, + didNotHydrateContainerInstance( + parentContainer: Container, + instance: Instance | TextInstance, + ) { + if (instance.nodeType === 1) { + warnForDeletedHydratableElement(parentContainer, (instance: any)); + } else { + warnForDeletedHydratableText(parentContainer, (instance: any)); + } + }, + didNotHydrateInstance( - parentInstance: Instance | Container, + parentType: string, + parentProps: Props, + parentInstance: Instance, instance: Instance | TextInstance, ) { if (instance.nodeType === 1) { @@ -534,8 +547,25 @@ var DOMRenderer = ReactFiberReconciler({ } }, + didNotFindHydratableContainerInstance( + parentContainer: Container, + type: string, + props: Props, + ) { + warnForInsertedHydratedElement(parentContainer, type, props); + }, + + didNotFindHydratableContainerTextInstance( + parentContainer: Container, + text: string, + ) { + warnForInsertedHydratedText(parentContainer, text); + }, + didNotFindHydratableInstance( - parentInstance: Instance | Container, + parentType: string, + parentProps: Props, + parentInstance: Instance, type: string, props: Props, ) { @@ -543,7 +573,9 @@ var DOMRenderer = ReactFiberReconciler({ }, didNotFindHydratableTextInstance( - parentInstance: Instance | Container, + parentType: string, + parentProps: Props, + parentInstance: Instance, text: string, ) { warnForInsertedHydratedText(parentInstance, text); diff --git a/src/renderers/shared/fiber/ReactFiberHydrationContext.js b/src/renderers/shared/fiber/ReactFiberHydrationContext.js index dee6f885200b1..3ad54bd48491f 100644 --- a/src/renderers/shared/fiber/ReactFiberHydrationContext.js +++ b/src/renderers/shared/fiber/ReactFiberHydrationContext.js @@ -44,7 +44,11 @@ module.exports = function( getFirstHydratableChild, hydrateInstance, hydrateTextInstance, + didNotHydrateContainerInstance, didNotHydrateInstance, + // TODO: These are currently unused, see below. + // didNotFindHydratableContainerInstance, + // didNotFindHydratableContainerTextInstance, didNotFindHydratableInstance, didNotFindHydratableTextInstance, } = config; @@ -57,7 +61,10 @@ module.exports = function( getFirstHydratableChild && hydrateInstance && hydrateTextInstance && + didNotHydrateContainerInstance && didNotHydrateInstance && + // didNotFindHydratableContainerInstance && + // didNotFindHydratableContainerTextInstance && didNotFindHydratableInstance && didNotFindHydratableTextInstance) ) { @@ -105,10 +112,18 @@ module.exports = function( if (__DEV__) { switch (returnFiber.tag) { case HostRoot: - didNotHydrateInstance(returnFiber.stateNode.containerInfo, instance); + didNotHydrateContainerInstance( + returnFiber.stateNode.containerInfo, + instance, + ); break; case HostComponent: - didNotHydrateInstance(returnFiber.stateNode, instance); + didNotHydrateInstance( + returnFiber.type, + returnFiber.memoizedProps, + returnFiber.stateNode, + instance, + ); break; } } @@ -134,32 +149,57 @@ module.exports = function( function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { fiber.effectTag |= Placement; if (__DEV__) { - var parentInstance; switch (returnFiber.tag) { // TODO: Currently we don't warn for insertions into the root because // we always insert into the root in the non-hydrating case. We just // delete the existing content. Reenable this once we have a better // strategy for determining if we're hydrating or not. - // case HostRoot: - // parentInstance = returnFiber.stateNode.containerInfo; + // case HostRoot: { + // const parentContainer = returnFiber.stateNode.containerInfo; + // switch (fiber.tag) { + // case HostComponent: + // const type = fiber.type; + // const props = fiber.pendingProps; + // didNotFindHydratableContainerInstance(parentContainer, type, props); + // break; + // case HostText: + // const text = fiber.pendingProps; + // didNotFindHydratableContainerTextInstance(parentContainer, text); + // break; + // } // break; - case HostComponent: - parentInstance = returnFiber.stateNode; + // } + case HostComponent: { + const parentType = returnFiber.type; + const parentProps = returnFiber.memoizedProps; + const parentInstance = returnFiber.stateNode; + switch (fiber.tag) { + case HostComponent: + const type = fiber.type; + const props = fiber.pendingProps; + didNotFindHydratableInstance( + parentType, + parentProps, + parentInstance, + type, + props, + ); + break; + case HostText: + const text = fiber.pendingProps; + didNotFindHydratableTextInstance( + parentType, + parentProps, + parentInstance, + text, + ); + break; + } break; + } default: return; } - switch (fiber.tag) { - case HostComponent: - const type = fiber.type; - const props = fiber.pendingProps; - didNotFindHydratableInstance(parentInstance, type, props); - break; - case HostText: - const text = fiber.pendingProps; - didNotFindHydratableTextInstance(parentInstance, text); - break; - } } } diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index eafa87add0c78..8e34b093c251c 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -139,14 +139,36 @@ export type HostConfig = { text: string, internalInstanceHandle: OpaqueHandle, ) => boolean, - didNotHydrateInstance?: (parentInstance: I | C, instance: I | TI) => void, + didNotHydrateContainerInstance?: ( + parentContainer: C, + instance: I | TI, + ) => void, + didNotHydrateInstance?: ( + parentType: T, + parentProps: P, + parentInstance: I, + instance: I | TI, + ) => void, + didNotFindHydratableContainerInstance?: ( + parentContainer: C, + type: T, + props: P, + ) => void, + didNotFindHydratableContainerTextInstance?: ( + parentContainer: C, + text: string, + ) => void, didNotFindHydratableInstance?: ( - parentInstance: I | C, + parentType: T, + parentProps: P, + parentInstance: I, type: T, props: P, ) => void, didNotFindHydratableTextInstance?: ( - parentInstance: I | C, + parentType: T, + parentProps: P, + parentInstance: I, text: string, ) => void, From ce62a4f40835a81c3c7d3eda41f11e3e22fb8511 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 4 Oct 2017 20:11:03 -0700 Subject: [PATCH 3/6] Suppress warning of inserted/deleted direct children --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index ea56474247cec..6335c96a792f6 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -58,6 +58,7 @@ var { var {precacheFiberNode, updateFiberProps} = ReactDOMComponentTree; if (__DEV__) { + var SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning'; var lowPriorityWarning = require('lowPriorityWarning'); var warning = require('fbjs/lib/warning'); var validateDOMNesting = require('validateDOMNesting'); @@ -99,6 +100,7 @@ type Props = { autoFocus?: boolean, children?: mixed, hidden?: boolean, + suppressHydrationWarning?: boolean, }; type Instance = Element; type TextInstance = Text; @@ -527,10 +529,12 @@ var DOMRenderer = ReactFiberReconciler({ parentContainer: Container, instance: Instance | TextInstance, ) { - if (instance.nodeType === 1) { - warnForDeletedHydratableElement(parentContainer, (instance: any)); - } else { - warnForDeletedHydratableText(parentContainer, (instance: any)); + if (__DEV__) { + if (instance.nodeType === 1) { + warnForDeletedHydratableElement(parentContainer, (instance: any)); + } else { + warnForDeletedHydratableText(parentContainer, (instance: any)); + } } }, @@ -540,10 +544,12 @@ var DOMRenderer = ReactFiberReconciler({ parentInstance: Instance, instance: Instance | TextInstance, ) { - if (instance.nodeType === 1) { - warnForDeletedHydratableElement(parentInstance, (instance: any)); - } else { - warnForDeletedHydratableText(parentInstance, (instance: any)); + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + if (instance.nodeType === 1) { + warnForDeletedHydratableElement(parentInstance, (instance: any)); + } else { + warnForDeletedHydratableText(parentInstance, (instance: any)); + } } }, @@ -552,14 +558,18 @@ var DOMRenderer = ReactFiberReconciler({ type: string, props: Props, ) { - warnForInsertedHydratedElement(parentContainer, type, props); + if (__DEV__) { + warnForInsertedHydratedElement(parentContainer, type, props); + } }, didNotFindHydratableContainerTextInstance( parentContainer: Container, text: string, ) { - warnForInsertedHydratedText(parentContainer, text); + if (__DEV__) { + warnForInsertedHydratedText(parentContainer, text); + } }, didNotFindHydratableInstance( @@ -569,7 +579,9 @@ var DOMRenderer = ReactFiberReconciler({ type: string, props: Props, ) { - warnForInsertedHydratedElement(parentInstance, type, props); + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + warnForInsertedHydratedElement(parentInstance, type, props); + } }, didNotFindHydratableTextInstance( @@ -578,7 +590,9 @@ var DOMRenderer = ReactFiberReconciler({ parentInstance: Instance, text: string, ) { - warnForInsertedHydratedText(parentInstance, text); + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + warnForInsertedHydratedText(parentInstance, text); + } }, scheduleDeferredCallback: ReactDOMFrameScheduling.rIC, From 979bed6333821dc81bd991fe6c12e2edf9ae11c7 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 4 Oct 2017 20:37:34 -0700 Subject: [PATCH 4/6] Add fixture testing hydration warning Also fixing the render->hydrate API change in the fixture --- fixtures/ssr/src/components/Page.js | 3 +++ fixtures/ssr/src/index.js | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/fixtures/ssr/src/components/Page.js b/fixtures/ssr/src/components/Page.js index 0284b6b39d811..806d1115c8207 100644 --- a/fixtures/ssr/src/components/Page.js +++ b/fixtures/ssr/src/components/Page.js @@ -15,6 +15,9 @@ export default class Page extends Component { ); return (
+

+ A random number: {Math.random()} +

{!this.state.active ? link : 'Thanks!'}

diff --git a/fixtures/ssr/src/index.js b/fixtures/ssr/src/index.js index ca551c8dadc9a..e4364cfbea28d 100644 --- a/fixtures/ssr/src/index.js +++ b/fixtures/ssr/src/index.js @@ -1,6 +1,6 @@ import React from 'react'; -import {render} from 'react-dom'; +import {hydrate} from 'react-dom'; import App from './components/App'; -render(, document); +hydrate(, document); From 71e4d7759aae0f4a43c2ce14b60514feb60e0572 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 5 Oct 2017 18:01:12 -0700 Subject: [PATCH 5/6] Add hooks when text hydration doesn't match up The purpose of these hooks is to pass the parent context to them. I don't want to do that in the normal hydrateTextInstance hooks since this is only used in DEV. This is also in line with what happens if there is no text instance at all and we invoke didNotFindHydratableTextInstance. --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 20 +++++++++ .../fiber/ReactFiberHydrationContext.js | 44 ++++++++++++++++--- .../shared/fiber/ReactFiberReconciler.js | 12 +++++ 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 6335c96a792f6..93bb2888fc83f 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -525,6 +525,26 @@ var DOMRenderer = ReactFiberReconciler({ return diffHydratedText(textInstance, text); }, + didNotMatchHydratedContainerTextInstance( + parentContainer: Container, + textInstance: TextInstance, + text: string, + ) { + if (__DEV__) { + } + }, + + didNotMatchHydratedTextInstance( + parentType: string, + parentProps: Props, + parentInstance: Instance, + textInstance: TextInstance, + text: string, + ) { + if (__DEV__) { + } + }, + didNotHydrateContainerInstance( parentContainer: Container, instance: Instance | TextInstance, diff --git a/src/renderers/shared/fiber/ReactFiberHydrationContext.js b/src/renderers/shared/fiber/ReactFiberHydrationContext.js index 3ad54bd48491f..879bee732eb53 100644 --- a/src/renderers/shared/fiber/ReactFiberHydrationContext.js +++ b/src/renderers/shared/fiber/ReactFiberHydrationContext.js @@ -44,6 +44,8 @@ module.exports = function( getFirstHydratableChild, hydrateInstance, hydrateTextInstance, + didNotMatchHydratedContainerTextInstance, + didNotMatchHydratedTextInstance, didNotHydrateContainerInstance, didNotHydrateInstance, // TODO: These are currently unused, see below. @@ -61,6 +63,8 @@ module.exports = function( getFirstHydratableChild && hydrateInstance && hydrateTextInstance && + didNotMatchHydratedContainerTextInstance && + didNotMatchHydratedTextInstance && didNotHydrateContainerInstance && didNotHydrateInstance && // didNotFindHydratableContainerInstance && @@ -283,11 +287,41 @@ module.exports = function( function prepareToHydrateHostTextInstance(fiber: Fiber): boolean { const textInstance: TI = fiber.stateNode; - const shouldUpdate = hydrateTextInstance( - textInstance, - fiber.memoizedProps, - fiber, - ); + const textContent: string = fiber.memoizedProps; + const shouldUpdate = hydrateTextInstance(textInstance, textContent, fiber); + if (__DEV__) { + if (shouldUpdate) { + // We assume that prepareToHydrateHostTextInstance is called in a context where the + // hydration parent is the parent host component of this host text. + const returnFiber = hydrationParentFiber; + if (returnFiber !== null) { + switch (returnFiber.tag) { + case HostRoot: { + const parentContainer = returnFiber.stateNode.containerInfo; + didNotMatchHydratedContainerTextInstance( + parentContainer, + textInstance, + textContent, + ); + break; + } + case HostComponent: { + const parentType = returnFiber.type; + const parentProps = returnFiber.memoizedProps; + const parentInstance = returnFiber.stateNode; + didNotMatchHydratedTextInstance( + parentType, + parentProps, + parentInstance, + textInstance, + textContent, + ); + break; + } + } + } + } + } return shouldUpdate; } diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 8e34b093c251c..0d7a76021a982 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -139,6 +139,18 @@ export type HostConfig = { text: string, internalInstanceHandle: OpaqueHandle, ) => boolean, + didNotMatchHydratedContainerTextInstance?: ( + parentContainer: C, + textInstance: TI, + text: string, + ) => void, + didNotMatchHydratedTextInstance?: ( + parentType: T, + parentProps: P, + parentInstance: I, + textInstance: TI, + text: string, + ) => void, didNotHydrateContainerInstance?: ( parentContainer: C, instance: I | TI, From b3dfd2d2aa5fd6aa37d6e6a51c2c525b5bcb24ae Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 5 Oct 2017 18:08:09 -0700 Subject: [PATCH 6/6] Move mismatch text hydration warning to the new hooks This lets us ignore this call when we have parent props available and the suppression flag is set. --- src/renderers/dom/fiber/ReactDOMFiberComponent.js | 9 +++++---- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 5 ++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberComponent.js b/src/renderers/dom/fiber/ReactDOMFiberComponent.js index 1058ada33e926..07a84c242ad28 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberComponent.js +++ b/src/renderers/dom/fiber/ReactDOMFiberComponent.js @@ -1140,12 +1140,13 @@ var ReactDOMFiberComponent = { diffHydratedText(textNode: Text, text: string): boolean { const isDifferent = textNode.nodeValue !== text; + return isDifferent; + }, + + warnForUnmatchedText(textNode: Text, text: string) { if (__DEV__) { - if (isDifferent) { - warnForTextDifference(textNode.nodeValue, text); - } + warnForTextDifference(textNode.nodeValue, text); } - return isDifferent; }, warnForDeletedHydratableElement( diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 93bb2888fc83f..d0dd466bfb4fb 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -50,6 +50,7 @@ var { updateProperties, diffHydratedProperties, diffHydratedText, + warnForUnmatchedText, warnForDeletedHydratableElement, warnForDeletedHydratableText, warnForInsertedHydratedElement, @@ -531,6 +532,7 @@ var DOMRenderer = ReactFiberReconciler({ text: string, ) { if (__DEV__) { + warnForUnmatchedText(textInstance, text); } }, @@ -541,7 +543,8 @@ var DOMRenderer = ReactFiberReconciler({ textInstance: TextInstance, text: string, ) { - if (__DEV__) { + if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) { + warnForUnmatchedText(textInstance, text); } },