Ignore SSR warning using explicit suppressHydrationWarning option (#11126)

* 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.

* 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.

* Suppress warning of inserted/deleted direct children

* Add fixture testing hydration warning

Also fixing the render->hydrate API change in the fixture

* 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.

* 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.
This commit is contained in:
Sebastian Markbåge 2017-10-06 19:50:01 -07:00 committed by GitHub
parent 5744571140
commit 4131af3e4b
11 changed files with 354 additions and 50 deletions

View File

@ -15,6 +15,9 @@ export default class Page extends Component {
);
return (
<div>
<p suppressHydrationWarning={true}>
A random number: {Math.random()}
</p>
<p>
{!this.state.active ? link : 'Thanks!'}
</p>

View File

@ -1,6 +1,6 @@
import React from 'react';
import {render} from 'react-dom';
import {hydrate} from 'react-dom';
import App from './components/App';
render(<App assets={window.assetManifest} />, document);
hydrate(<App assets={window.assetManifest} />, document);

View File

@ -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<mixed> {
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);
}
@ -1125,14 +1140,15 @@ var ReactDOMFiberComponent = {
diffHydratedText(textNode: Text, text: string): boolean {
const isDifferent = textNode.nodeValue !== text;
if (__DEV__) {
if (isDifferent) {
warnForTextDifference(textNode.nodeValue, text);
}
}
return isDifferent;
},
warnForUnmatchedText(textNode: Text, text: string) {
if (__DEV__) {
warnForTextDifference(textNode.nodeValue, text);
}
},
warnForDeletedHydratableElement(
parentNode: Element | Document,
child: Element,

View File

@ -50,6 +50,7 @@ var {
updateProperties,
diffHydratedProperties,
diffHydratedText,
warnForUnmatchedText,
warnForDeletedHydratableElement,
warnForDeletedHydratableText,
warnForInsertedHydratedElement,
@ -58,6 +59,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 +101,7 @@ type Props = {
autoFocus?: boolean,
children?: mixed,
hidden?: boolean,
suppressHydrationWarning?: boolean,
};
type Instance = Element;
type TextInstance = Text;
@ -523,30 +526,96 @@ var DOMRenderer = ReactFiberReconciler({
return diffHydratedText(textInstance, text);
},
didNotHydrateInstance(
parentInstance: Instance | Container,
didNotMatchHydratedContainerTextInstance(
parentContainer: Container,
textInstance: TextInstance,
text: string,
) {
if (__DEV__) {
warnForUnmatchedText(textInstance, text);
}
},
didNotMatchHydratedTextInstance(
parentType: string,
parentProps: Props,
parentInstance: Instance,
textInstance: TextInstance,
text: string,
) {
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
warnForUnmatchedText(textInstance, text);
}
},
didNotHydrateContainerInstance(
parentContainer: Container,
instance: Instance | TextInstance,
) {
if (instance.nodeType === 1) {
warnForDeletedHydratableElement(parentInstance, (instance: any));
} else {
warnForDeletedHydratableText(parentInstance, (instance: any));
if (__DEV__) {
if (instance.nodeType === 1) {
warnForDeletedHydratableElement(parentContainer, (instance: any));
} else {
warnForDeletedHydratableText(parentContainer, (instance: any));
}
}
},
didNotHydrateInstance(
parentType: string,
parentProps: Props,
parentInstance: Instance,
instance: Instance | TextInstance,
) {
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
if (instance.nodeType === 1) {
warnForDeletedHydratableElement(parentInstance, (instance: any));
} else {
warnForDeletedHydratableText(parentInstance, (instance: any));
}
}
},
didNotFindHydratableContainerInstance(
parentContainer: Container,
type: string,
props: Props,
) {
if (__DEV__) {
warnForInsertedHydratedElement(parentContainer, type, props);
}
},
didNotFindHydratableContainerTextInstance(
parentContainer: Container,
text: string,
) {
if (__DEV__) {
warnForInsertedHydratedText(parentContainer, text);
}
},
didNotFindHydratableInstance(
parentInstance: Instance | Container,
parentType: string,
parentProps: Props,
parentInstance: Instance,
type: string,
props: Props,
) {
warnForInsertedHydratedElement(parentInstance, type, props);
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
warnForInsertedHydratedElement(parentInstance, type, props);
}
},
didNotFindHydratableTextInstance(
parentInstance: Instance | Container,
parentType: string,
parentProps: Props,
parentInstance: Instance,
text: string,
) {
warnForInsertedHydratedText(parentInstance, text);
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
warnForInsertedHydratedText(parentInstance, text);
}
},
scheduleDeferredCallback: ReactDOMFrameScheduling.rIC,

View File

@ -21,6 +21,7 @@ var RESERVED_PROPS = {
defaultChecked: true,
innerHTML: true,
suppressContentEditableWarning: true,
suppressHydrationWarning: true,
style: true,
};

View File

@ -318,6 +318,7 @@ describe('ReactDOMComponent', () => {
<my-component
children={['foo']}
suppressContentEditableWarning={true}
suppressHydrationWarning={true}
/>,
container,
);
@ -325,11 +326,15 @@ describe('ReactDOMComponent', () => {
expect(
container.firstChild.hasAttribute('suppressContentEditableWarning'),
).toBe(false);
expect(
container.firstChild.hasAttribute('suppressHydrationWarning'),
).toBe(false);
ReactDOM.render(
<my-component
children={['bar']}
suppressContentEditableWarning={false}
suppressHydrationWarning={false}
/>,
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', () => {

View File

@ -725,6 +725,16 @@ describe('ReactDOMServerIntegration', () => {
);
expect(e.getAttribute('dangerouslySetInnerHTML')).toBe(null);
});
itRenders('no suppressContentEditableWarning attribute', async render => {
const e = await render(<div suppressContentEditableWarning={true} />);
expect(e.getAttribute('suppressContentEditableWarning')).toBe(null);
});
itRenders('no suppressHydrationWarning attribute', async render => {
const e = await render(<span suppressHydrationWarning={true} />);
expect(e.getAttribute('suppressHydrationWarning')).toBe(null);
});
});
describe('inline styles', function() {
@ -2623,6 +2633,36 @@ describe('ReactDOMServerIntegration', () => {
it('should error reconnecting different attribute values', () =>
expectMarkupMismatch(<div id="foo" />, <div id="bar" />));
it('can explicitly ignore errors reconnecting different element types of children', () =>
expectMarkupMatch(
<div><div /></div>,
<div suppressHydrationWarning={true}><span /></div>,
));
it('can explicitly ignore errors reconnecting missing attributes', () =>
expectMarkupMatch(
<div id="foo" />,
<div suppressHydrationWarning={true} />,
));
it('can explicitly ignore errors reconnecting added attributes', () =>
expectMarkupMatch(
<div />,
<div id="foo" suppressHydrationWarning={true} />,
));
it('can explicitly ignore errors reconnecting different attribute values', () =>
expectMarkupMatch(
<div id="foo" />,
<div id="bar" suppressHydrationWarning={true} />,
));
it('can not deeply ignore errors reconnecting different attribute values', () =>
expectMarkupMismatch(
<div><div id="foo" /></div>,
<div suppressHydrationWarning={true}><div id="bar" /></div>,
));
});
describe('inline styles', function() {
@ -2661,6 +2701,18 @@ describe('ReactDOMServerIntegration', () => {
<div style={{width: '1px', fontSize: '2px'}} />,
<div style={{fontSize: '2px', width: '1px'}} />,
));
it('can explicitly ignore errors reconnecting added style values', () =>
expectMarkupMatch(
<div style={{}} />,
<div style={{width: '1px'}} suppressHydrationWarning={true} />,
));
it('can explicitly ignore reconnecting different style values', () =>
expectMarkupMatch(
<div style={{width: '1px'}} />,
<div style={{width: '2px'}} suppressHydrationWarning={true} />,
));
});
describe('text nodes', function() {
@ -2681,6 +2733,18 @@ describe('ReactDOMServerIntegration', () => {
<div>{'Text1'}{'Text2'}</div>,
<div>{'Text1'}{'Text3'}</div>,
));
it('can explicitly ignore reconnecting different text', () =>
expectMarkupMatch(
<div>Text</div>,
<div suppressHydrationWarning={true}>Other Text</div>,
));
it('can explicitly ignore reconnecting different text in two code blocks', () =>
expectMarkupMatch(
<div suppressHydrationWarning={true}>{'Text1'}{'Text2'}</div>,
<div suppressHydrationWarning={true}>{'Text1'}{'Text3'}</div>,
));
});
describe('element trees and children', function() {
@ -2731,6 +2795,30 @@ describe('ReactDOMServerIntegration', () => {
it('can distinguish an empty component from an empty text component', () =>
expectMarkupMatch(<div><EmptyComponent /></div>, <div>{''}</div>));
it('can explicitly ignore reconnecting more children', () =>
expectMarkupMatch(
<div><div /></div>,
<div suppressHydrationWarning={true}><div /><div /></div>,
));
it('can explicitly ignore reconnecting fewer children', () =>
expectMarkupMatch(
<div><div /><div /></div>,
<div suppressHydrationWarning={true}><div /></div>,
));
it('can explicitly ignore reconnecting reordered children', () =>
expectMarkupMatch(
<div suppressHydrationWarning={true}><div /><span /></div>,
<div suppressHydrationWarning={true}><span /><div /></div>,
));
it('can not deeply ignore reconnecting reordered children', () =>
expectMarkupMismatch(
<div suppressHydrationWarning={true}><div><div /><span /></div></div>,
<div suppressHydrationWarning={true}><div><span /><div /></div></div>,
));
});
// Markup Mismatches: misc
@ -2739,5 +2827,14 @@ describe('ReactDOMServerIntegration', () => {
<div dangerouslySetInnerHTML={{__html: "<span id='child1'/>"}} />,
<div dangerouslySetInnerHTML={{__html: "<span id='child2'/>"}} />,
));
it('can explicitly ignore reconnecting a div with different dangerouslySetInnerHTML', () =>
expectMarkupMatch(
<div dangerouslySetInnerHTML={{__html: "<span id='child1'/>"}} />,
<div
dangerouslySetInnerHTML={{__html: "<span id='child2'/>"}}
suppressHydrationWarning={true}
/>,
));
});
});

View File

@ -401,6 +401,7 @@ var possibleStandardNames = {
strokeopacity: 'strokeOpacity',
'stroke-opacity': 'strokeOpacity',
suppresscontenteditablewarning: 'suppressContentEditableWarning',
suppresshydrationwarning: 'suppressHydrationWarning',
surfacescale: 'surfaceScale',
systemlanguage: 'systemLanguage',
tablevalues: 'tableValues',

View File

@ -44,7 +44,13 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
getFirstHydratableChild,
hydrateInstance,
hydrateTextInstance,
didNotMatchHydratedContainerTextInstance,
didNotMatchHydratedTextInstance,
didNotHydrateContainerInstance,
didNotHydrateInstance,
// TODO: These are currently unused, see below.
// didNotFindHydratableContainerInstance,
// didNotFindHydratableContainerTextInstance,
didNotFindHydratableInstance,
didNotFindHydratableTextInstance,
} = config;
@ -57,7 +63,12 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
getFirstHydratableChild &&
hydrateInstance &&
hydrateTextInstance &&
didNotMatchHydratedContainerTextInstance &&
didNotMatchHydratedTextInstance &&
didNotHydrateContainerInstance &&
didNotHydrateInstance &&
// didNotFindHydratableContainerInstance &&
// didNotFindHydratableContainerTextInstance &&
didNotFindHydratableInstance &&
didNotFindHydratableTextInstance)
) {
@ -105,10 +116,18 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
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 +153,57 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
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;
}
}
}
@ -243,11 +287,41 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
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;
}

View File

@ -140,14 +140,48 @@ export type HostConfig<T, P, I, TI, PI, C, CX, PL> = {
text: string,
internalInstanceHandle: OpaqueHandle,
) => boolean,
didNotHydrateInstance?: (parentInstance: I | C, instance: I | TI) => void,
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,
) => 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,

View File

@ -251,6 +251,7 @@ var RESERVED_PROPS = {
children: null,
dangerouslySetInnerHTML: null,
suppressContentEditableWarning: null,
suppressHydrationWarning: null,
};
function createOpenTagMarkup(