mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[Float][Fizz][Fiber] - Do not hoist elements with itemProp & hydrate more tolerantly in hoist contexts (#26256)
## Do not hoist elements with `itemProp` In HTML `itemprop` signifies a property of an `itemscope` with respect to the Microdata spec (https://html.spec.whatwg.org/multipage/microdata.html#microdata) additionally `itemprop` is valid on any tag and can even make some tags that are otherwise invalid in the `<body>` valid there (`<meta>` for instance). Originally I tried an approach where if you rendered something otherwise hoistable inside an `itemscope` it would not hoist if it had an `itemprop`. This meant that some components with `itemprop` could hoist (if they were not scoped, which is generally invalid microdata implementation). However the problem is things that do hoist, hoist into the head and body and these tags can have an `itemscope`. This creates a ton of ambiguity when trying to hydrate in these hoist scopes because we can't know for certain whether a DOM node we find there was hoisted or not even if it has an `itemprop` attribute. There are other scenarios too that have abiguous semantics like rendering a hoistable with `itemProp` outside of `<html itemScope={true>`. Is it fair to embed that hoistable inside that itemScope even though it was defined outside? To simplify the situation and disambiguate I dropped the `itemscope` portion from the implementation and now any host component that could normally be hoisted will not hoist if it has an `itemProp` prop. In addition to the changes made for `itemProp` this PR also modifies part of the hydration implementation to be more tolerant of tags injected by 3rd parties. This was opportunistically done when we needed to have context information like `inItemScope` but with the most recent implementation that has been removed. I have however left the hydration changes in place as it is a goal to make React handle hydrating the entire Document even when we cannot control whether 3rd parties are going to inject tags that React will not render but are also not hoistables ------- ##### Original Description when we considered tracking itemScope >One recent decision was to make elements using the `itemProp` prop not hoistable if they were inside and itemScope. This better fits with Microdata spec which allows for meta tags and other tag types usually reserved for the `<head>` to be used in the `<body>` when using itemScope. > >To implement this a number of small changes were necessary > >1. HostContext in prod needed to expand beyond just tracking the element namespace for new element creation. It now tracks whether we are in an itemScope. To keep this efficient it is modeled as a bitmask. >2. To disambiguate what is and is not a potential instance in the DOM for hoistables the hydration algo was updated to skip past non-matching instances while attempting to claim the instance rather than ahead of time (getNextHydratable). >3. React will not consider an itemScope on `<html>`, `<head>`, or `<body>` as a valid scope for the hoisting opt-out. This is important as an invariant so we can make assumptions about certain tags in these scopes. This should not be a functional breaking change because if any of these tags have an `itemScope` then it can just be moved into the first node inside the `<body>` > >Since we were already updating the logic for hydration to better support `itemScope` opt-out I also changed the hydration behavior for suspected 3rd party nodes in `<head>` and `<body>`. Now if you are hydrating in either of those contexts hydration will skip past any non-matching nodes until it finds a match. This allows 3rd party scripts and extensions to inject nodes in either context that React does not expect and still avoid a hydration mismatch. > >This new algorithm isn't perfect and it is possible for a mismatch to occur. The most glaring case may be if a 3rd party script prepends a `<div>` into `<body>` and you render a `<div>` in `<body>` in your app. there is nothing to signal to React that this div was 3rd party so it will claim is as the hydrated instance and hydration will almost certainly fail immediately afterwards. > >The expectation is that this is rare and that if falling back to client rendering is transparent to the user then there is not problem here. We will continue to evaluate this and may change the hydration matching algorithm further to match user and developer expectations
This commit is contained in:
+97
-92
@@ -55,7 +55,12 @@ import {
|
||||
setValueForStyles,
|
||||
validateShorthandPropertyCollisionInDev,
|
||||
} from './CSSPropertyOperations';
|
||||
import {HTML_NAMESPACE, getIntrinsicNamespace} from '../shared/DOMNamespaces';
|
||||
import {
|
||||
HTML_NAMESPACE,
|
||||
MATH_NAMESPACE,
|
||||
SVG_NAMESPACE,
|
||||
getIntrinsicNamespace,
|
||||
} from '../shared/DOMNamespaces';
|
||||
import {
|
||||
getPropertyInfo,
|
||||
shouldIgnoreAttribute,
|
||||
@@ -375,112 +380,112 @@ function updateDOMProperties(
|
||||
}
|
||||
}
|
||||
|
||||
export function createElement(
|
||||
// Creates elements in the HTML namesapce
|
||||
export function createHTMLElement(
|
||||
type: string,
|
||||
props: Object,
|
||||
rootContainerElement: Element | Document | DocumentFragment,
|
||||
parentNamespace: string,
|
||||
ownerDocument: Document,
|
||||
): Element {
|
||||
let isCustomComponentTag;
|
||||
|
||||
// We create tags in the namespace of their parent container, except HTML
|
||||
// tags get no namespace.
|
||||
const ownerDocument: Document =
|
||||
getOwnerDocumentFromRootContainer(rootContainerElement);
|
||||
let domElement: Element;
|
||||
let namespaceURI = parentNamespace;
|
||||
if (namespaceURI === HTML_NAMESPACE) {
|
||||
namespaceURI = getIntrinsicNamespace(type);
|
||||
if (__DEV__) {
|
||||
isCustomComponentTag = isCustomComponent(type, props);
|
||||
// Should this check be gated by parent namespace? Not sure we want to
|
||||
// allow <SVG> or <mATH>.
|
||||
if (!isCustomComponentTag && type !== type.toLowerCase()) {
|
||||
console.error(
|
||||
'<%s /> is using incorrect casing. ' +
|
||||
'Use PascalCase for React components, ' +
|
||||
'or lowercase for HTML elements.',
|
||||
type,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (namespaceURI === HTML_NAMESPACE) {
|
||||
if (__DEV__) {
|
||||
isCustomComponentTag = isCustomComponent(type, props);
|
||||
// Should this check be gated by parent namespace? Not sure we want to
|
||||
// allow <SVG> or <mATH>.
|
||||
if (!isCustomComponentTag && type !== type.toLowerCase()) {
|
||||
console.error(
|
||||
'<%s /> is using incorrect casing. ' +
|
||||
'Use PascalCase for React components, ' +
|
||||
'or lowercase for HTML elements.',
|
||||
type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'script') {
|
||||
// Create the script via .innerHTML so its "parser-inserted" flag is
|
||||
// set to true and it does not execute
|
||||
const div = ownerDocument.createElement('div');
|
||||
if (__DEV__) {
|
||||
if (enableTrustedTypesIntegration && !didWarnScriptTags) {
|
||||
console.error(
|
||||
'Encountered a script tag while rendering React component. ' +
|
||||
'Scripts inside React components are never executed when rendering ' +
|
||||
'on the client. Consider using template tag instead ' +
|
||||
'(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).',
|
||||
);
|
||||
didWarnScriptTags = true;
|
||||
}
|
||||
}
|
||||
div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
|
||||
// This is guaranteed to yield a script element.
|
||||
const firstChild = ((div.firstChild: any): HTMLScriptElement);
|
||||
domElement = div.removeChild(firstChild);
|
||||
} else if (typeof props.is === 'string') {
|
||||
domElement = ownerDocument.createElement(type, {is: props.is});
|
||||
} else {
|
||||
// Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
|
||||
// See discussion in https://github.com/facebook/react/pull/6896
|
||||
// and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
|
||||
domElement = ownerDocument.createElement(type);
|
||||
// Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` and `size`
|
||||
// attributes on `select`s needs to be added before `option`s are inserted.
|
||||
// This prevents:
|
||||
// - a bug where the `select` does not scroll to the correct option because singular
|
||||
// `select` elements automatically pick the first item #13222
|
||||
// - a bug where the `select` set the first item as selected despite the `size` attribute #14239
|
||||
// See https://github.com/facebook/react/issues/13222
|
||||
// and https://github.com/facebook/react/issues/14239
|
||||
if (type === 'select') {
|
||||
const node = ((domElement: any): HTMLSelectElement);
|
||||
if (props.multiple) {
|
||||
node.multiple = true;
|
||||
} else if (props.size) {
|
||||
// Setting a size greater than 1 causes a select to behave like `multiple=true`, where
|
||||
// it is possible that no option is selected.
|
||||
//
|
||||
// This is only necessary when a select in "single selection mode".
|
||||
node.size = props.size;
|
||||
}
|
||||
if (type === 'script') {
|
||||
// Create the script via .innerHTML so its "parser-inserted" flag is
|
||||
// set to true and it does not execute
|
||||
const div = ownerDocument.createElement('div');
|
||||
if (__DEV__) {
|
||||
if (enableTrustedTypesIntegration && !didWarnScriptTags) {
|
||||
console.error(
|
||||
'Encountered a script tag while rendering React component. ' +
|
||||
'Scripts inside React components are never executed when rendering ' +
|
||||
'on the client. Consider using template tag instead ' +
|
||||
'(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).',
|
||||
);
|
||||
didWarnScriptTags = true;
|
||||
}
|
||||
}
|
||||
div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
|
||||
// This is guaranteed to yield a script element.
|
||||
const firstChild = ((div.firstChild: any): HTMLScriptElement);
|
||||
domElement = div.removeChild(firstChild);
|
||||
} else if (typeof props.is === 'string') {
|
||||
domElement = ownerDocument.createElement(type, {is: props.is});
|
||||
} else {
|
||||
domElement = ownerDocument.createElementNS(namespaceURI, type);
|
||||
// Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
|
||||
// See discussion in https://github.com/facebook/react/pull/6896
|
||||
// and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
|
||||
domElement = ownerDocument.createElement(type);
|
||||
// Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` and `size`
|
||||
// attributes on `select`s needs to be added before `option`s are inserted.
|
||||
// This prevents:
|
||||
// - a bug where the `select` does not scroll to the correct option because singular
|
||||
// `select` elements automatically pick the first item #13222
|
||||
// - a bug where the `select` set the first item as selected despite the `size` attribute #14239
|
||||
// See https://github.com/facebook/react/issues/13222
|
||||
// and https://github.com/facebook/react/issues/14239
|
||||
if (type === 'select') {
|
||||
const node = ((domElement: any): HTMLSelectElement);
|
||||
if (props.multiple) {
|
||||
node.multiple = true;
|
||||
} else if (props.size) {
|
||||
// Setting a size greater than 1 causes a select to behave like `multiple=true`, where
|
||||
// it is possible that no option is selected.
|
||||
//
|
||||
// This is only necessary when a select in "single selection mode".
|
||||
node.size = props.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
if (namespaceURI === HTML_NAMESPACE) {
|
||||
if (
|
||||
!isCustomComponentTag &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
Object.prototype.toString.call(domElement) ===
|
||||
'[object HTMLUnknownElement]' &&
|
||||
!hasOwnProperty.call(warnedUnknownTags, type)
|
||||
) {
|
||||
warnedUnknownTags[type] = true;
|
||||
console.error(
|
||||
'The tag <%s> is unrecognized in this browser. ' +
|
||||
'If you meant to render a React component, start its name with ' +
|
||||
'an uppercase letter.',
|
||||
type,
|
||||
);
|
||||
}
|
||||
if (
|
||||
!isCustomComponentTag &&
|
||||
// $FlowFixMe[method-unbinding]
|
||||
Object.prototype.toString.call(domElement) ===
|
||||
'[object HTMLUnknownElement]' &&
|
||||
!hasOwnProperty.call(warnedUnknownTags, type)
|
||||
) {
|
||||
warnedUnknownTags[type] = true;
|
||||
console.error(
|
||||
'The tag <%s> is unrecognized in this browser. ' +
|
||||
'If you meant to render a React component, start its name with ' +
|
||||
'an uppercase letter.',
|
||||
type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return domElement;
|
||||
}
|
||||
|
||||
export function createSVGElement(
|
||||
type: string,
|
||||
ownerDocument: Document,
|
||||
): Element {
|
||||
return ownerDocument.createElementNS(SVG_NAMESPACE, type);
|
||||
}
|
||||
|
||||
export function createMathElement(
|
||||
type: string,
|
||||
ownerDocument: Document,
|
||||
): Element {
|
||||
return ownerDocument.createElementNS(MATH_NAMESPACE, type);
|
||||
}
|
||||
|
||||
export function createTextNode(
|
||||
text: string,
|
||||
rootContainerElement: Element | Document | DocumentFragment,
|
||||
@@ -864,9 +869,9 @@ export function diffHydratedProperties(
|
||||
domElement: Element,
|
||||
tag: string,
|
||||
rawProps: Object,
|
||||
parentNamespace: string,
|
||||
isConcurrentMode: boolean,
|
||||
shouldWarnDev: boolean,
|
||||
parentNamespaceDev: string,
|
||||
): null | Array<mixed> {
|
||||
let isCustomComponentTag;
|
||||
let extraAttributeNames: Set<string>;
|
||||
@@ -1109,11 +1114,11 @@ export function diffHydratedProperties(
|
||||
propertyInfo,
|
||||
);
|
||||
} else {
|
||||
let ownNamespace = parentNamespace;
|
||||
if (ownNamespace === HTML_NAMESPACE) {
|
||||
ownNamespace = getIntrinsicNamespace(tag);
|
||||
let ownNamespaceDev = parentNamespaceDev;
|
||||
if (ownNamespaceDev === HTML_NAMESPACE) {
|
||||
ownNamespaceDev = getIntrinsicNamespace(tag);
|
||||
}
|
||||
if (ownNamespace === HTML_NAMESPACE) {
|
||||
if (ownNamespaceDev === HTML_NAMESPACE) {
|
||||
// $FlowFixMe - Should be inferred as not undefined.
|
||||
extraAttributeNames.delete(propKey.toLowerCase());
|
||||
} else {
|
||||
|
||||
+29
-48
@@ -9,30 +9,32 @@
|
||||
|
||||
import type {Instance, Container} from './ReactDOMHostConfig';
|
||||
|
||||
import {isAttributeNameSafe} from '../shared/DOMProperty';
|
||||
import {precacheFiberNode} from './ReactDOMComponentTree';
|
||||
import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
|
||||
|
||||
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
|
||||
const {Dispatcher} = ReactDOMSharedInternals;
|
||||
import {
|
||||
checkAttributeStringCoercion,
|
||||
checkPropStringCoercion,
|
||||
} from 'shared/CheckStringCoercion';
|
||||
|
||||
import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
|
||||
import {isAttributeNameSafe} from '../shared/DOMProperty';
|
||||
import {SVG_NAMESPACE} from '../shared/DOMNamespaces';
|
||||
import {
|
||||
validatePreloadArguments,
|
||||
validatePreinitArguments,
|
||||
getValueDescriptorExpectingObjectForWarning,
|
||||
getValueDescriptorExpectingEnumForWarning,
|
||||
} from '../shared/ReactDOMResourceValidation';
|
||||
import {createElement, setInitialProperties} from './ReactDOMComponent';
|
||||
import {
|
||||
checkAttributeStringCoercion,
|
||||
checkPropStringCoercion,
|
||||
} from 'shared/CheckStringCoercion';
|
||||
|
||||
import {precacheFiberNode} from './ReactDOMComponentTree';
|
||||
import {createHTMLElement, setInitialProperties} from './ReactDOMComponent';
|
||||
import {
|
||||
getResourcesFromRoot,
|
||||
isMarkedResource,
|
||||
markNodeAsResource,
|
||||
} from './ReactDOMComponentTree';
|
||||
import {HTML_NAMESPACE, SVG_NAMESPACE} from '../shared/DOMNamespaces';
|
||||
import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
|
||||
|
||||
// The resource types we support. currently they match the form for the as argument.
|
||||
// In the future this may need to change, especially when modules / scripts are supported
|
||||
@@ -174,11 +176,10 @@ function preconnectAs(
|
||||
|
||||
const preconnectProps = {rel, crossOrigin, href};
|
||||
if (null === ownerDocument.querySelector(key)) {
|
||||
const preloadInstance = createElement(
|
||||
const preloadInstance = createHTMLElement(
|
||||
'link',
|
||||
preconnectProps,
|
||||
ownerDocument,
|
||||
HTML_NAMESPACE,
|
||||
);
|
||||
setInitialProperties(preloadInstance, 'link', preconnectProps);
|
||||
markNodeAsResource(preloadInstance);
|
||||
@@ -289,11 +290,10 @@ function preload(href: string, options: PreloadOptions) {
|
||||
preloadPropsMap.set(key, preloadProps);
|
||||
|
||||
if (null === ownerDocument.querySelector(preloadKey)) {
|
||||
const preloadInstance = createElement(
|
||||
const preloadInstance = createHTMLElement(
|
||||
'link',
|
||||
preloadProps,
|
||||
ownerDocument,
|
||||
HTML_NAMESPACE,
|
||||
);
|
||||
setInitialProperties(preloadInstance, 'link', preloadProps);
|
||||
markNodeAsResource(preloadInstance);
|
||||
@@ -371,11 +371,10 @@ function preinit(href: string, options: PreinitOptions) {
|
||||
preloadPropsMap.set(key, preloadProps);
|
||||
|
||||
if (null === preloadDocument.querySelector(preloadKey)) {
|
||||
const preloadInstance = createElement(
|
||||
const preloadInstance = createHTMLElement(
|
||||
'link',
|
||||
preloadProps,
|
||||
preloadDocument,
|
||||
HTML_NAMESPACE,
|
||||
);
|
||||
setInitialProperties(preloadInstance, 'link', preloadProps);
|
||||
markNodeAsResource(preloadInstance);
|
||||
@@ -417,12 +416,8 @@ function preinit(href: string, options: PreinitOptions) {
|
||||
if (preloadProps) {
|
||||
adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
|
||||
}
|
||||
instance = createElement(
|
||||
'link',
|
||||
stylesheetProps,
|
||||
resourceRoot,
|
||||
HTML_NAMESPACE,
|
||||
);
|
||||
const ownerDocument = getDocumentFromRoot(resourceRoot);
|
||||
instance = createHTMLElement('link', stylesheetProps, ownerDocument);
|
||||
markNodeAsResource(instance);
|
||||
setInitialProperties(instance, 'link', stylesheetProps);
|
||||
insertStylesheet(instance, precedence, resourceRoot);
|
||||
@@ -463,12 +458,8 @@ function preinit(href: string, options: PreinitOptions) {
|
||||
if (preloadProps) {
|
||||
adoptPreloadPropsForScript(scriptProps, preloadProps);
|
||||
}
|
||||
instance = createElement(
|
||||
'script',
|
||||
scriptProps,
|
||||
resourceRoot,
|
||||
HTML_NAMESPACE,
|
||||
);
|
||||
const ownerDocument = getDocumentFromRoot(resourceRoot);
|
||||
instance = createHTMLElement('script', scriptProps, ownerDocument);
|
||||
markNodeAsResource(instance);
|
||||
setInitialProperties(instance, 'link', scriptProps);
|
||||
(getDocumentFromRoot(resourceRoot).head: any).appendChild(instance);
|
||||
@@ -703,11 +694,10 @@ function preloadStylesheet(
|
||||
null ===
|
||||
ownerDocument.querySelector(getPreloadStylesheetSelectorFromKey(key))
|
||||
) {
|
||||
const preloadInstance = createElement(
|
||||
const preloadInstance = createHTMLElement(
|
||||
'link',
|
||||
preloadProps,
|
||||
ownerDocument,
|
||||
HTML_NAMESPACE,
|
||||
);
|
||||
setInitialProperties(preloadInstance, 'link', preloadProps);
|
||||
markNodeAsResource(preloadInstance);
|
||||
@@ -762,16 +752,13 @@ export function acquireResource(
|
||||
);
|
||||
if (instance) {
|
||||
resource.instance = instance;
|
||||
markNodeAsResource(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
const styleProps = styleTagPropsFromRawProps(props);
|
||||
instance = createElement(
|
||||
'style',
|
||||
styleProps,
|
||||
hoistableRoot,
|
||||
HTML_NAMESPACE,
|
||||
);
|
||||
const ownerDocument = getDocumentFromRoot(hoistableRoot);
|
||||
instance = createHTMLElement('style', styleProps, ownerDocument);
|
||||
|
||||
markNodeAsResource(instance);
|
||||
setInitialProperties(instance, 'style', styleProps);
|
||||
@@ -793,6 +780,7 @@ export function acquireResource(
|
||||
);
|
||||
if (instance) {
|
||||
resource.instance = instance;
|
||||
markNodeAsResource(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -803,12 +791,8 @@ export function acquireResource(
|
||||
}
|
||||
|
||||
// Construct and insert a new instance
|
||||
instance = createElement(
|
||||
'link',
|
||||
stylesheetProps,
|
||||
hoistableRoot,
|
||||
HTML_NAMESPACE,
|
||||
);
|
||||
const ownerDocument = getDocumentFromRoot(hoistableRoot);
|
||||
instance = createHTMLElement('link', stylesheetProps, ownerDocument);
|
||||
markNodeAsResource(instance);
|
||||
const linkInstance: HTMLLinkElement = (instance: any);
|
||||
(linkInstance: any)._p = new Promise((resolve, reject) => {
|
||||
@@ -837,6 +821,7 @@ export function acquireResource(
|
||||
);
|
||||
if (instance) {
|
||||
resource.instance = instance;
|
||||
markNodeAsResource(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -848,12 +833,8 @@ export function acquireResource(
|
||||
}
|
||||
|
||||
// Construct and insert a new instance
|
||||
instance = createElement(
|
||||
'script',
|
||||
scriptProps,
|
||||
hoistableRoot,
|
||||
HTML_NAMESPACE,
|
||||
);
|
||||
const ownerDocument = getDocumentFromRoot(hoistableRoot);
|
||||
instance = createHTMLElement('script', scriptProps, ownerDocument);
|
||||
markNodeAsResource(instance);
|
||||
setInitialProperties(instance, 'link', scriptProps);
|
||||
(getDocumentFromRoot(hoistableRoot).head: any).appendChild(instance);
|
||||
@@ -1092,7 +1073,7 @@ export function hydrateHoistable(
|
||||
}
|
||||
|
||||
// There is no matching instance to hydrate, we create it now
|
||||
const instance = createElement(type, props, ownerDocument, HTML_NAMESPACE);
|
||||
const instance = createHTMLElement(type, props, ownerDocument);
|
||||
setInitialProperties(instance, type, props);
|
||||
precacheFiberNode(internalInstanceHandle, instance);
|
||||
markNodeAsResource(instance);
|
||||
|
||||
+211
-155
@@ -32,7 +32,9 @@ import {
|
||||
export {detachDeletedInstance};
|
||||
import {hasRole} from './DOMAccessibilityRoles';
|
||||
import {
|
||||
createElement,
|
||||
createHTMLElement,
|
||||
createSVGElement,
|
||||
createMathElement,
|
||||
createTextNode,
|
||||
setInitialProperties,
|
||||
diffProperties,
|
||||
@@ -58,6 +60,7 @@ import {
|
||||
import {
|
||||
getChildNamespace,
|
||||
SVG_NAMESPACE,
|
||||
MATH_NAMESPACE,
|
||||
HTML_NAMESPACE,
|
||||
} from '../shared/DOMNamespaces';
|
||||
import {
|
||||
@@ -144,7 +147,7 @@ export interface SuspenseInstance extends Comment {
|
||||
export type HydratableInstance = Instance | TextInstance | SuspenseInstance;
|
||||
export type PublicInstance = Element | Text;
|
||||
type HostContextDev = {
|
||||
namespace: string,
|
||||
namespace: HostContextProd,
|
||||
ancestorInfo: AncestorInfoDev,
|
||||
};
|
||||
type HostContextProd = string;
|
||||
@@ -178,7 +181,7 @@ export function getRootHostContext(
|
||||
rootContainerInstance: Container,
|
||||
): HostContext {
|
||||
let type;
|
||||
let namespace;
|
||||
let namespace: HostContextProd;
|
||||
const nodeType = rootContainerInstance.nodeType;
|
||||
switch (nodeType) {
|
||||
case DOCUMENT_NODE:
|
||||
@@ -274,12 +277,10 @@ export function createHoistableInstance(
|
||||
rootContainerInstance: Container,
|
||||
internalInstanceHandle: Object,
|
||||
): Instance {
|
||||
const domElement: Instance = createElement(
|
||||
type,
|
||||
props,
|
||||
const ownerDocument = getOwnerDocumentFromRootContainer(
|
||||
rootContainerInstance,
|
||||
HTML_NAMESPACE,
|
||||
);
|
||||
const domElement: Instance = createHTMLElement(type, props, ownerDocument);
|
||||
precacheFiberNode(internalInstanceHandle, domElement);
|
||||
updateFiberProps(domElement, props);
|
||||
setInitialProperties(domElement, type, props);
|
||||
@@ -294,10 +295,10 @@ export function createInstance(
|
||||
hostContext: HostContext,
|
||||
internalInstanceHandle: Object,
|
||||
): Instance {
|
||||
let parentNamespace: string;
|
||||
let namespace;
|
||||
if (__DEV__) {
|
||||
// TODO: take namespace into account when validating.
|
||||
const hostContextDev = ((hostContext: any): HostContextDev);
|
||||
const hostContextDev: HostContextDev = (hostContext: any);
|
||||
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
|
||||
if (
|
||||
typeof props.children === 'string' ||
|
||||
@@ -310,16 +311,37 @@ export function createInstance(
|
||||
);
|
||||
validateDOMNesting(null, string, ownAncestorInfo);
|
||||
}
|
||||
parentNamespace = hostContextDev.namespace;
|
||||
namespace = hostContextDev.namespace;
|
||||
} else {
|
||||
parentNamespace = ((hostContext: any): HostContextProd);
|
||||
const hostContextProd: HostContextProd = (hostContext: any);
|
||||
namespace = hostContextProd;
|
||||
}
|
||||
const domElement: Instance = createElement(
|
||||
type,
|
||||
props,
|
||||
|
||||
const ownerDocument = getOwnerDocumentFromRootContainer(
|
||||
rootContainerInstance,
|
||||
parentNamespace,
|
||||
);
|
||||
|
||||
let domElement: Instance;
|
||||
create: switch (namespace) {
|
||||
case SVG_NAMESPACE:
|
||||
domElement = createSVGElement(type, ownerDocument);
|
||||
break;
|
||||
case MATH_NAMESPACE:
|
||||
domElement = createMathElement(type, ownerDocument);
|
||||
break;
|
||||
case HTML_NAMESPACE:
|
||||
switch (type) {
|
||||
case 'svg':
|
||||
domElement = createSVGElement(type, ownerDocument);
|
||||
break create;
|
||||
case 'math':
|
||||
domElement = createMathElement(type, ownerDocument);
|
||||
break create;
|
||||
}
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
default:
|
||||
domElement = createHTMLElement(type, props, ownerDocument);
|
||||
}
|
||||
precacheFiberNode(internalInstanceHandle, domElement);
|
||||
updateFiberProps(domElement, props);
|
||||
return domElement;
|
||||
@@ -825,17 +847,9 @@ export const supportsHydration = true;
|
||||
|
||||
// With Resources, some HostComponent types will never be server rendered and need to be
|
||||
// inserted without breaking hydration
|
||||
export function isHydratable(type: string, props: Props): boolean {
|
||||
export function isHydratableType(type: string, props: Props): boolean {
|
||||
if (enableFloat) {
|
||||
if (type === 'link') {
|
||||
if (
|
||||
(props: any).rel === 'stylesheet' &&
|
||||
typeof (props: any).precedence !== 'string'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else if (type === 'script') {
|
||||
if (type === 'script') {
|
||||
const {async, onLoad, onError} = (props: any);
|
||||
return !(async && (onLoad || onError));
|
||||
}
|
||||
@@ -844,6 +858,129 @@ export function isHydratable(type: string, props: Props): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
export function isHydratableText(text: string): boolean {
|
||||
return text !== '';
|
||||
}
|
||||
|
||||
export function shouldSkipHydratableForInstance(
|
||||
instance: HydratableInstance,
|
||||
type: string,
|
||||
props: Props,
|
||||
): boolean {
|
||||
if (instance.nodeType !== ELEMENT_NODE) {
|
||||
// This is a suspense boundary or Text node.
|
||||
// Suspense Boundaries are never expected to be injected by 3rd parties. If we see one it should be matched
|
||||
// and this is a hydration error.
|
||||
// Text Nodes are also not expected to be injected by 3rd parties. This is less of a guarantee for <body>
|
||||
// but it seems reasonable and conservative to reject this as a hydration error as well
|
||||
return false;
|
||||
} else if (
|
||||
instance.nodeName.toLowerCase() !== type.toLowerCase() ||
|
||||
isMarkedResource(instance)
|
||||
) {
|
||||
// We are either about to
|
||||
return true;
|
||||
} else {
|
||||
// We have an Element with the right type.
|
||||
const element: Element = (instance: any);
|
||||
const anyProps = (props: any);
|
||||
|
||||
// We are going to try to exclude it if we can definitely identify it as a hoisted Node or if
|
||||
// we can guess that the node is likely hoisted or was inserted by a 3rd party script or browser extension
|
||||
// using high entropy attributes for certain types. This technique will fail for strange insertions like
|
||||
// extension prepending <div> in the <body> but that already breaks before and that is an edge case.
|
||||
switch (type) {
|
||||
// case 'title':
|
||||
//We assume all titles are matchable. You should only have one in the Document, at least in a hoistable scope
|
||||
// and if you are a HostComponent with type title we must either be in an <svg> context or this title must have an `itemProp` prop.
|
||||
case 'meta': {
|
||||
// The only way to opt out of hoisting meta tags is to give it an itemprop attribute. We assume there will be
|
||||
// not 3rd party meta tags that are prepended, accepting the cases where this isn't true because meta tags
|
||||
// are usually only functional for SSR so even in a rare case where we did bind to an injected tag the runtime
|
||||
// implications are minimal
|
||||
if (!element.hasAttribute('itemprop')) {
|
||||
// This is a Hoistable
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'link': {
|
||||
// Links come in many forms and we do expect 3rd parties to inject them into <head> / <body>. We exclude known resources
|
||||
// and then use high-entroy attributes like href which are almost always used and almost always unique to filter out unlikely
|
||||
// matches.
|
||||
const rel = element.getAttribute('rel');
|
||||
if (rel === 'stylesheet' && element.hasAttribute('data-precedence')) {
|
||||
// This is a stylesheet resource
|
||||
return true;
|
||||
} else if (
|
||||
rel !== anyProps.rel ||
|
||||
element.getAttribute('href') !==
|
||||
(anyProps.href == null ? null : anyProps.href) ||
|
||||
element.getAttribute('crossorigin') !==
|
||||
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ||
|
||||
element.getAttribute('title') !==
|
||||
(anyProps.title == null ? null : anyProps.title)
|
||||
) {
|
||||
// rel + href should usually be enough to uniquely identify a link however crossOrigin can vary for rel preconnect
|
||||
// and title could vary for rel alternate
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'style': {
|
||||
// Styles are hard to match correctly. We can exclude known resources but otherwise we accept the fact that a non-hoisted style tags
|
||||
// in <head> or <body> are likely never going to be unmounted given their position in the document and the fact they likely hold global styles
|
||||
if (element.hasAttribute('data-precedence')) {
|
||||
// This is a style resource
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'script': {
|
||||
// Scripts are a little tricky, we exclude known resources and then similar to links try to use high-entropy attributes
|
||||
// to reject poor matches. One challenge with scripts are inline scripts. We don't attempt to check text content which could
|
||||
// in theory lead to a hydration error later if a 3rd party injected an inline script before the React rendered nodes.
|
||||
// Falling back to client rendering if this happens should be seemless though so we will try this hueristic and revisit later
|
||||
// if we learn it is problematic
|
||||
const srcAttr = element.getAttribute('src');
|
||||
if (
|
||||
srcAttr &&
|
||||
element.hasAttribute('async') &&
|
||||
!element.hasAttribute('itemprop')
|
||||
) {
|
||||
// This is an async script resource
|
||||
return true;
|
||||
} else if (
|
||||
srcAttr !== (anyProps.src == null ? null : anyProps.src) ||
|
||||
element.getAttribute('type') !==
|
||||
(anyProps.type == null ? null : anyProps.type) ||
|
||||
element.getAttribute('crossorigin') !==
|
||||
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin)
|
||||
) {
|
||||
// This script is for a different src
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// We have excluded the most likely cases of mismatch between hoistable tags, 3rd party script inserted tags,
|
||||
// and browser extension inserted tags. While it is possible this is not the right match it is a decent hueristic
|
||||
// that should work in the vast majority of cases.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldSkipHydratableForTextInstance(
|
||||
instance: HydratableInstance,
|
||||
): boolean {
|
||||
return instance.nodeType === ELEMENT_NODE;
|
||||
}
|
||||
|
||||
export function shouldSkipHydratableForSuspenseInstance(
|
||||
instance: HydratableInstance,
|
||||
): boolean {
|
||||
return instance.nodeType === ELEMENT_NODE;
|
||||
}
|
||||
|
||||
export function canHydrateInstance(
|
||||
instance: HydratableInstance,
|
||||
@@ -852,19 +989,21 @@ export function canHydrateInstance(
|
||||
): null | Instance {
|
||||
if (
|
||||
instance.nodeType !== ELEMENT_NODE ||
|
||||
type.toLowerCase() !== instance.nodeName.toLowerCase()
|
||||
instance.nodeName.toLowerCase() !== type.toLowerCase()
|
||||
) {
|
||||
return null;
|
||||
} else {
|
||||
return ((instance: any): Instance);
|
||||
}
|
||||
// This has now been refined to an element node.
|
||||
return ((instance: any): Instance);
|
||||
}
|
||||
|
||||
export function canHydrateTextInstance(
|
||||
instance: HydratableInstance,
|
||||
text: string,
|
||||
): null | TextInstance {
|
||||
if (text === '' || instance.nodeType !== TEXT_NODE) {
|
||||
if (text === '') return null;
|
||||
|
||||
if (instance.nodeType !== TEXT_NODE) {
|
||||
// Empty strings are not parsed by HTML so there won't be a correct match here.
|
||||
return null;
|
||||
}
|
||||
@@ -876,7 +1015,6 @@ export function canHydrateSuspenseInstance(
|
||||
instance: HydratableInstance,
|
||||
): null | SuspenseInstance {
|
||||
if (instance.nodeType !== COMMENT_NODE) {
|
||||
// Empty strings are not parsed by HTML so there won't be a correct match here.
|
||||
return null;
|
||||
}
|
||||
// This has now been refined to a suspense node.
|
||||
@@ -931,114 +1069,8 @@ function getNextHydratable(node: ?Node) {
|
||||
// Skip non-hydratable nodes.
|
||||
for (; node != null; node = ((node: any): Node).nextSibling) {
|
||||
const nodeType = node.nodeType;
|
||||
if (enableFloat && enableHostSingletons) {
|
||||
if (nodeType === ELEMENT_NODE) {
|
||||
const element: Element = (node: any);
|
||||
switch (element.tagName) {
|
||||
// This is subtle. in SVG scope the title tag is case sensitive. we don't want to skip
|
||||
// titles in svg but we do want to skip them outside of svg. there is an edge case where
|
||||
// you could do `React.createElement('TITLE', ...)` inside an svg scope but the SSR serializer
|
||||
// will still emit lowercase. Practically speaking the only time the DOM will have a non-uppercased
|
||||
// title tagName is if it is inside an svg.
|
||||
// Other Resource types like META, BASE, LINK, and SCRIPT should be treated as resources even inside
|
||||
// svg scope because they are invalid otherwise. We still don't need to handle the lowercase variant
|
||||
// because if they are present in the DOM already they would have been hoisted outside the SVG scope
|
||||
// as Resources. So while it would be correct to skip a <link> inside <svg> and this algorithm won't
|
||||
// skip that link because the tagName will not be uppercased it functionally is irrelevant. If one
|
||||
// tries to render incompatible types such as a non-resource stylesheet inside an svg the server will
|
||||
// emit that invalid html and hydration will fail. In Dev this will present warnings guiding the
|
||||
// developer on how to fix.
|
||||
case 'TITLE':
|
||||
case 'META':
|
||||
case 'HTML':
|
||||
case 'HEAD':
|
||||
case 'BODY': {
|
||||
continue;
|
||||
}
|
||||
case 'LINK': {
|
||||
const linkEl: HTMLLinkElement = (element: any);
|
||||
// All links that are server rendered are resources except
|
||||
// stylesheets that do not have a precedence
|
||||
if (
|
||||
linkEl.rel === 'stylesheet' &&
|
||||
!linkEl.hasAttribute('data-precedence')
|
||||
) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
case 'STYLE': {
|
||||
const styleEl: HTMLStyleElement = (element: any);
|
||||
if (styleEl.hasAttribute('data-precedence')) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'SCRIPT': {
|
||||
const scriptEl: HTMLScriptElement = (element: any);
|
||||
if (scriptEl.hasAttribute('async')) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
} else if (nodeType === TEXT_NODE) {
|
||||
break;
|
||||
}
|
||||
} else if (enableFloat) {
|
||||
if (nodeType === ELEMENT_NODE) {
|
||||
const element: Element = (node: any);
|
||||
switch (element.tagName) {
|
||||
case 'TITLE':
|
||||
case 'META': {
|
||||
continue;
|
||||
}
|
||||
case 'LINK': {
|
||||
const linkEl: HTMLLinkElement = (element: any);
|
||||
// All links that are server rendered are resources except
|
||||
// stylesheets that do not have a precedence
|
||||
if (
|
||||
linkEl.rel === 'stylesheet' &&
|
||||
!linkEl.hasAttribute('data-precedence')
|
||||
) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
case 'STYLE': {
|
||||
const styleEl: HTMLStyleElement = (element: any);
|
||||
if (styleEl.hasAttribute('data-precedence')) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'SCRIPT': {
|
||||
const scriptEl: HTMLScriptElement = (element: any);
|
||||
if (scriptEl.hasAttribute('async')) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
} else if (nodeType === TEXT_NODE) {
|
||||
break;
|
||||
}
|
||||
} else if (enableHostSingletons) {
|
||||
if (nodeType === ELEMENT_NODE) {
|
||||
const tag: string = (node: any).tagName;
|
||||
if (tag === 'HTML' || tag === 'HEAD' || tag === 'BODY') {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
} else if (nodeType === TEXT_NODE) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) {
|
||||
break;
|
||||
}
|
||||
if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) {
|
||||
break;
|
||||
}
|
||||
if (nodeType === COMMENT_NODE) {
|
||||
const nodeData = (node: any).data;
|
||||
@@ -1093,26 +1125,28 @@ export function hydrateInstance(
|
||||
// TODO: Possibly defer this until the commit phase where all the events
|
||||
// get attached.
|
||||
updateFiberProps(instance, props);
|
||||
let parentNamespace: string;
|
||||
if (__DEV__) {
|
||||
const hostContextDev = ((hostContext: any): HostContextDev);
|
||||
parentNamespace = hostContextDev.namespace;
|
||||
} else {
|
||||
parentNamespace = ((hostContext: any): HostContextProd);
|
||||
}
|
||||
|
||||
// TODO: Temporary hack to check if we're in a concurrent root. We can delete
|
||||
// when the legacy root API is removed.
|
||||
const isConcurrentMode =
|
||||
((internalInstanceHandle: Fiber).mode & ConcurrentMode) !== NoMode;
|
||||
|
||||
let parentNamespace;
|
||||
if (__DEV__) {
|
||||
const hostContextDev = ((hostContext: any): HostContextDev);
|
||||
parentNamespace = hostContextDev.namespace;
|
||||
} else {
|
||||
const hostContextProd = ((hostContext: any): HostContextProd);
|
||||
parentNamespace = hostContextProd;
|
||||
}
|
||||
|
||||
return diffHydratedProperties(
|
||||
instance,
|
||||
type,
|
||||
props,
|
||||
parentNamespace,
|
||||
isConcurrentMode,
|
||||
shouldWarnDev,
|
||||
parentNamespace,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1584,7 +1618,7 @@ export function isHostHoistableType(
|
||||
hostContext: HostContext,
|
||||
): boolean {
|
||||
let outsideHostContainerContext: boolean;
|
||||
let namespace: string;
|
||||
let namespace: HostContextProd;
|
||||
if (__DEV__) {
|
||||
const hostContextDev: HostContextDev = (hostContext: any);
|
||||
// We can only render resources when we are not within the host container context
|
||||
@@ -1595,17 +1629,41 @@ export function isHostHoistableType(
|
||||
const hostContextProd: HostContextProd = (hostContext: any);
|
||||
namespace = hostContextProd;
|
||||
}
|
||||
|
||||
// Global opt out of hoisting for anything in SVG Namespace or anything with an itemProp inside an itemScope
|
||||
if (namespace === SVG_NAMESPACE || props.itemProp != null) {
|
||||
if (__DEV__) {
|
||||
if (
|
||||
outsideHostContainerContext &&
|
||||
props.itemProp != null &&
|
||||
(type === 'meta' ||
|
||||
type === 'title' ||
|
||||
type === 'style' ||
|
||||
type === 'link' ||
|
||||
type === 'script')
|
||||
) {
|
||||
console.error(
|
||||
'Cannot render a <%s> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an' +
|
||||
' `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <%s> remove the `itemProp` prop.' +
|
||||
' Otherwise, try moving this tag into the <head> or <body> of the Document.',
|
||||
type,
|
||||
type,
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'meta':
|
||||
case 'title': {
|
||||
return namespace !== SVG_NAMESPACE;
|
||||
return true;
|
||||
}
|
||||
case 'style': {
|
||||
if (
|
||||
typeof props.precedence !== 'string' ||
|
||||
typeof props.href !== 'string' ||
|
||||
props.href === '' ||
|
||||
namespace === SVG_NAMESPACE
|
||||
props.href === ''
|
||||
) {
|
||||
if (__DEV__) {
|
||||
if (outsideHostContainerContext) {
|
||||
@@ -1629,8 +1687,7 @@ export function isHostHoistableType(
|
||||
typeof props.href !== 'string' ||
|
||||
props.href === '' ||
|
||||
props.onLoad ||
|
||||
props.onError ||
|
||||
namespace === SVG_NAMESPACE
|
||||
props.onError
|
||||
) {
|
||||
if (__DEV__) {
|
||||
if (
|
||||
@@ -1686,8 +1743,7 @@ export function isHostHoistableType(
|
||||
props.onLoad ||
|
||||
props.onError ||
|
||||
typeof props.src !== 'string' ||
|
||||
!props.src ||
|
||||
namespace === SVG_NAMESPACE
|
||||
!props.src
|
||||
) {
|
||||
if (__DEV__) {
|
||||
if (outsideHostContainerContext) {
|
||||
@@ -1771,8 +1827,8 @@ export function resolveSingletonInstance(
|
||||
validateDOMNestingDev: boolean,
|
||||
): Instance {
|
||||
if (__DEV__) {
|
||||
const hostContextDev = ((hostContext: any): HostContextDev);
|
||||
if (validateDOMNestingDev) {
|
||||
const hostContextDev = ((hostContext: any): HostContextDev);
|
||||
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1257,7 +1257,11 @@ function pushMeta(
|
||||
noscriptTagInScope: boolean,
|
||||
): null {
|
||||
if (enableFloat) {
|
||||
if (insertionMode === SVG_MODE || noscriptTagInScope) {
|
||||
if (
|
||||
insertionMode === SVG_MODE ||
|
||||
noscriptTagInScope ||
|
||||
props.itemProp != null
|
||||
) {
|
||||
return pushSelfClosing(target, props, 'meta');
|
||||
} else {
|
||||
if (textEmbedded) {
|
||||
@@ -1293,6 +1297,7 @@ function pushLink(
|
||||
if (
|
||||
insertionMode === SVG_MODE ||
|
||||
noscriptTagInScope ||
|
||||
props.itemProp != null ||
|
||||
typeof rel !== 'string' ||
|
||||
typeof href !== 'string' ||
|
||||
href === ''
|
||||
@@ -1573,6 +1578,7 @@ function pushStyle(
|
||||
if (
|
||||
insertionMode === SVG_MODE ||
|
||||
noscriptTagInScope ||
|
||||
props.itemProp != null ||
|
||||
typeof precedence !== 'string' ||
|
||||
typeof href !== 'string' ||
|
||||
href === ''
|
||||
@@ -1843,7 +1849,11 @@ function pushTitle(
|
||||
}
|
||||
|
||||
if (enableFloat) {
|
||||
if (insertionMode !== SVG_MODE && !noscriptTagInScope) {
|
||||
if (
|
||||
insertionMode !== SVG_MODE &&
|
||||
!noscriptTagInScope &&
|
||||
props.itemProp == null
|
||||
) {
|
||||
pushTitleImpl(responseState.hoistableChunks, props);
|
||||
return null;
|
||||
} else {
|
||||
@@ -2034,6 +2044,7 @@ function pushScript(
|
||||
if (
|
||||
insertionMode === SVG_MODE ||
|
||||
noscriptTagInScope ||
|
||||
props.itemProp != null ||
|
||||
typeof props.src !== 'string' ||
|
||||
!props.src
|
||||
) {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @noflow
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
||||
|
||||
+336
-1
@@ -369,7 +369,6 @@ describe('ReactDOMFloat', () => {
|
||||
<meta property="foo" content="bar" />
|
||||
<title>foo</title>
|
||||
<link rel="foo" href="bar" />
|
||||
<link rel="foo" href="bar" />
|
||||
<noscript><link rel="icon" href="icon"/></noscript>
|
||||
<base target="foo" href="bar" />
|
||||
<script async="" src="foo" />
|
||||
@@ -2304,6 +2303,342 @@ body {
|
||||
);
|
||||
});
|
||||
|
||||
it('does not hoist anything with an itemprop prop', async () => {
|
||||
function App() {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<meta itemProp="outside" content="unscoped" />
|
||||
<link itemProp="link" rel="foo" href="foo" />
|
||||
<title itemProp="outside-title">title</title>
|
||||
<link
|
||||
itemProp="outside-stylesheet"
|
||||
rel="stylesheet"
|
||||
href="bar"
|
||||
precedence="default"
|
||||
/>
|
||||
<style itemProp="outside-style" href="baz" precedence="default">
|
||||
outside style
|
||||
</style>
|
||||
<script itemProp="outside-script" async={true} src="qux" />
|
||||
</head>
|
||||
<body>
|
||||
<div itemScope={true}>
|
||||
<div>
|
||||
<meta itemProp="inside-meta" content="scoped" />
|
||||
<link itemProp="inside-link" rel="foo" href="foo" />
|
||||
<title itemProp="inside-title">title</title>
|
||||
<link
|
||||
itemProp="inside-stylesheet"
|
||||
rel="stylesheet"
|
||||
href="bar"
|
||||
precedence="default"
|
||||
/>
|
||||
<style itemProp="inside-style" href="baz" precedence="default">
|
||||
inside style
|
||||
</style>
|
||||
<script itemProp="inside-script" async={true} src="qux" />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
await actIntoEmptyDocument(() => {
|
||||
renderToPipeableStream(<App />).pipe(writable);
|
||||
});
|
||||
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<meta itemprop="outside" content="unscoped" />
|
||||
<link itemprop="link" rel="foo" href="foo" />
|
||||
<title itemprop="outside-title">title</title>
|
||||
<link
|
||||
itemprop="outside-stylesheet"
|
||||
rel="stylesheet"
|
||||
href="bar"
|
||||
precedence="default"
|
||||
/>
|
||||
<style itemprop="outside-style" href="baz" precedence="default">
|
||||
outside style
|
||||
</style>
|
||||
<script itemprop="outside-script" async="" src="qux" />
|
||||
</head>
|
||||
<body>
|
||||
<div itemscope="">
|
||||
<div>
|
||||
<meta itemprop="inside-meta" content="scoped" />
|
||||
<link itemprop="inside-link" rel="foo" href="foo" />
|
||||
<title itemprop="inside-title">title</title>
|
||||
<link
|
||||
itemprop="inside-stylesheet"
|
||||
rel="stylesheet"
|
||||
href="bar"
|
||||
precedence="default"
|
||||
/>
|
||||
<style itemprop="inside-style" href="baz" precedence="default">
|
||||
inside style
|
||||
</style>
|
||||
<script itemprop="inside-script" async="" src="qux" />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
ReactDOMClient.hydrateRoot(document, <App />);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<meta itemprop="outside" content="unscoped" />
|
||||
<link itemprop="link" rel="foo" href="foo" />
|
||||
<title itemprop="outside-title">title</title>
|
||||
<link
|
||||
itemprop="outside-stylesheet"
|
||||
rel="stylesheet"
|
||||
href="bar"
|
||||
precedence="default"
|
||||
/>
|
||||
<style itemprop="outside-style" href="baz" precedence="default">
|
||||
outside style
|
||||
</style>
|
||||
<script itemprop="outside-script" async="" src="qux" />
|
||||
</head>
|
||||
<body>
|
||||
<div itemscope="">
|
||||
<div>
|
||||
<meta itemprop="inside-meta" content="scoped" />
|
||||
<link itemprop="inside-link" rel="foo" href="foo" />
|
||||
<title itemprop="inside-title">title</title>
|
||||
<link
|
||||
itemprop="inside-stylesheet"
|
||||
rel="stylesheet"
|
||||
href="bar"
|
||||
precedence="default"
|
||||
/>
|
||||
<style itemprop="inside-style" href="baz" precedence="default">
|
||||
inside style
|
||||
</style>
|
||||
<script itemprop="inside-script" async="" src="qux" />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
it('warns if you render a tag with itemProp outside <body> or <head>', async () => {
|
||||
const root = ReactDOMClient.createRoot(document);
|
||||
root.render(
|
||||
<html>
|
||||
<meta itemProp="foo" />
|
||||
<title itemProp="foo">title</title>
|
||||
<style itemProp="foo">style</style>
|
||||
<link itemProp="foo" />
|
||||
<script itemProp="foo" />
|
||||
</html>,
|
||||
);
|
||||
expect(() => {
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
}).toErrorDev([
|
||||
'Cannot render a <meta> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <meta> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
|
||||
'Cannot render a <title> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <title> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
|
||||
'Cannot render a <style> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <style> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
|
||||
'Cannot render a <link> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <link> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
|
||||
'Cannot render a <script> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <script> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
|
||||
'validateDOMNesting(...): <meta> cannot appear as a child of <html>',
|
||||
'validateDOMNesting(...): <title> cannot appear as a child of <html>',
|
||||
'validateDOMNesting(...): <style> cannot appear as a child of <html>',
|
||||
'validateDOMNesting(...): <link> cannot appear as a child of <html>',
|
||||
'validateDOMNesting(...): <script> cannot appear as a child of <html>',
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate enableFloat
|
||||
it('can hydrate resources and components in the head and body even if a browser or 3rd party script injects extra html nodes', async () => {
|
||||
// This is a stress test case for hydrating a complex combination of hoistable elements, hoistable resources and host components
|
||||
// in an environment that has been manipulated by 3rd party scripts/extensions to modify the <head> and <body>
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<link rel="foo" href="foo" />
|
||||
<script async={true} src="rendered" />
|
||||
<link rel="stylesheet" href="stylesheet" precedence="default" />
|
||||
<html itemScope={true}>
|
||||
<head>
|
||||
{/* Component */}
|
||||
<link rel="stylesheet" href="stylesheet" />
|
||||
<script src="sync rendered" data-meaningful="" />
|
||||
<style>{'body { background-color: red; }'}</style>
|
||||
<script src="async rendered" async={true} onLoad={() => {}} />
|
||||
<noscript>
|
||||
<meta name="noscript" content="noscript" />
|
||||
</noscript>
|
||||
<link rel="foo" href="foo" onLoad={() => {}} />
|
||||
</head>
|
||||
<body>
|
||||
{/* Component because it has itemProp */}
|
||||
<meta name="foo" content="foo" itemProp="a prop" />
|
||||
{/* regular Hoistable */}
|
||||
<meta name="foo" content="foo" />
|
||||
{/* regular Hoistable */}
|
||||
<title>title</title>
|
||||
<div itemScope={true}>
|
||||
<div>
|
||||
<div>deep hello</div>
|
||||
{/* Component because it has itemProp */}
|
||||
<meta name="foo" content="foo" itemProp="a prop" />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<link rel="foo" href="foo" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
await actIntoEmptyDocument(() => {
|
||||
renderToPipeableStream(<App />).pipe(writable);
|
||||
});
|
||||
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html itemscope="">
|
||||
<head>
|
||||
{/* Hoisted Resources and elements */}
|
||||
<link rel="stylesheet" href="stylesheet" data-precedence="default" />
|
||||
<script async="" src="rendered" />
|
||||
<link rel="preload" as="script" href="sync rendered" />
|
||||
<link rel="preload" as="script" href="async rendered" />
|
||||
<link rel="foo" href="foo" />
|
||||
<meta name="foo" content="foo" />
|
||||
<title>title</title>
|
||||
<link rel="foo" href="foo" />
|
||||
{/* rendered host components */}
|
||||
<link rel="stylesheet" href="stylesheet" />
|
||||
<script src="sync rendered" data-meaningful="" />
|
||||
<style>{'body { background-color: red; }'}</style>
|
||||
<noscript><meta name="noscript" content="noscript"/></noscript>
|
||||
<link rel="foo" href="foo" />
|
||||
</head>
|
||||
<body>
|
||||
<meta name="foo" content="foo" itemprop="a prop" />
|
||||
<div itemscope="">
|
||||
<div>
|
||||
<div>deep hello</div>
|
||||
<meta name="foo" content="foo" itemprop="a prop" />
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
// We inject some styles, divs, scripts into the begginning, middle, and end
|
||||
// of the head / body.
|
||||
const injectedStyle = document.createElement('style');
|
||||
injectedStyle.textContent = 'body { background-color: blue; }';
|
||||
document.head.prepend(injectedStyle.cloneNode(true));
|
||||
document.head.appendChild(injectedStyle.cloneNode(true));
|
||||
document.body.prepend(injectedStyle.cloneNode(true));
|
||||
document.body.appendChild(injectedStyle.cloneNode(true));
|
||||
|
||||
const injectedDiv = document.createElement('div');
|
||||
document.head.prepend(injectedDiv);
|
||||
document.head.appendChild(injectedDiv.cloneNode(true));
|
||||
// We do not prepend a <div> in body because this will conflict with hyration
|
||||
// We still mostly hydrate by matchign tag and <div> does not have any attributes to
|
||||
// differentiate between likely-inject and likely-rendered cases. If a <div> is prepended
|
||||
// in the <body> and you render a <div> as the first child of <body> there will be a conflict.
|
||||
// We consider this a rare edge case and even if it does happen the fallback to client rendering
|
||||
// should patch up the DOM correctly
|
||||
document.body.appendChild(injectedDiv.cloneNode(true));
|
||||
|
||||
const injectedScript = document.createElement('script');
|
||||
injectedScript.setAttribute('async', '');
|
||||
injectedScript.setAttribute('src', 'injected');
|
||||
document.head.prepend(injectedScript);
|
||||
document.head.appendChild(injectedScript.cloneNode(true));
|
||||
document.body.prepend(injectedScript.cloneNode(true));
|
||||
document.body.appendChild(injectedScript.cloneNode(true));
|
||||
|
||||
// We hydrate the same App and confirm the output is identical except for the async
|
||||
// script insertion that happens because we do not SSR async scripts with load handlers.
|
||||
// All the extra inject nodes are preset
|
||||
const root = ReactDOMClient.hydrateRoot(document, <App />);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html itemscope="">
|
||||
<head>
|
||||
<script async="" src="injected" />
|
||||
<div />
|
||||
<style>{'body { background-color: blue; }'}</style>
|
||||
<link rel="stylesheet" href="stylesheet" data-precedence="default" />
|
||||
<script async="" src="rendered" />
|
||||
<link rel="preload" as="script" href="sync rendered" />
|
||||
<link rel="preload" as="script" href="async rendered" />
|
||||
<link rel="foo" href="foo" />
|
||||
<meta name="foo" content="foo" />
|
||||
<title>title</title>
|
||||
<link rel="foo" href="foo" />
|
||||
<link rel="stylesheet" href="stylesheet" />
|
||||
<script src="sync rendered" data-meaningful="" />
|
||||
<style>{'body { background-color: red; }'}</style>
|
||||
<script src="async rendered" async="" />
|
||||
<noscript><meta name="noscript" content="noscript"/></noscript>
|
||||
<link rel="foo" href="foo" />
|
||||
<style>{'body { background-color: blue; }'}</style>
|
||||
<div />
|
||||
<script async="" src="injected" />
|
||||
</head>
|
||||
<body>
|
||||
<script async="" src="injected" />
|
||||
<style>{'body { background-color: blue; }'}</style>
|
||||
<meta name="foo" content="foo" itemprop="a prop" />
|
||||
<div itemscope="">
|
||||
<div>
|
||||
<div>deep hello</div>
|
||||
<meta name="foo" content="foo" itemprop="a prop" />
|
||||
</div>
|
||||
</div>
|
||||
<style>{'body { background-color: blue; }'}</style>
|
||||
<div />
|
||||
<script async="" src="injected" />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
// We unmount. The nodes that remain are
|
||||
// 1. Hoisted resources (we don't clean these up on unmount to address races with streaming suspense and navigation)
|
||||
// 2. preloads that are injected to hint the browser to load a resource but are not associated to Fibers directly
|
||||
// 3. Nodes that React skipped over during hydration
|
||||
root.unmount();
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<script async="" src="injected" />
|
||||
<div />
|
||||
<style>{'body { background-color: blue; }'}</style>
|
||||
<link rel="stylesheet" href="stylesheet" data-precedence="default" />
|
||||
<script async="" src="rendered" />
|
||||
<link rel="preload" as="script" href="sync rendered" />
|
||||
<link rel="preload" as="script" href="async rendered" />
|
||||
<style>{'body { background-color: blue; }'}</style>
|
||||
<div />
|
||||
<script async="" src="injected" />
|
||||
</head>
|
||||
<body>
|
||||
<script async="" src="injected" />
|
||||
<style>{'body { background-color: blue; }'}</style>
|
||||
<style>{'body { background-color: blue; }'}</style>
|
||||
<div />
|
||||
<script async="" src="injected" />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
describe('ReactDOM.prefetchDNS(href)', () => {
|
||||
it('creates a dns-prefetch resource when called', async () => {
|
||||
function App({url}) {
|
||||
|
||||
@@ -3380,7 +3380,6 @@ describe('ReactDOMServerPartialHydration', () => {
|
||||
itHydratesWithoutMismatch('an empty string in class component', TestAppClass);
|
||||
|
||||
it('fallback to client render on hydration mismatch at root', async () => {
|
||||
let isClient = false;
|
||||
let suspend = true;
|
||||
let resolve;
|
||||
const promise = new Promise((res, rej) => {
|
||||
@@ -3389,36 +3388,35 @@ describe('ReactDOMServerPartialHydration', () => {
|
||||
res();
|
||||
};
|
||||
});
|
||||
function App() {
|
||||
function App({isClient}) {
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<ChildThatSuspends id={1} />
|
||||
<ChildThatSuspends id={1} isClient={isClient} />
|
||||
</Suspense>
|
||||
{isClient ? <span>client</span> : <div>server</div>}
|
||||
<Suspense fallback={<div>Loading</div>}>
|
||||
<ChildThatSuspends id={2} />
|
||||
<ChildThatSuspends id={2} isClient={isClient} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
function ChildThatSuspends({id}) {
|
||||
function ChildThatSuspends({id, isClient}) {
|
||||
if (isClient && suspend) {
|
||||
throw promise;
|
||||
}
|
||||
return <div>{id}</div>;
|
||||
}
|
||||
|
||||
const finalHTML = ReactDOMServer.renderToString(<App />);
|
||||
const finalHTML = ReactDOMServer.renderToString(<App isClient={false} />);
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
container.innerHTML = finalHTML;
|
||||
isClient = true;
|
||||
|
||||
expect(() => {
|
||||
act(() => {
|
||||
ReactDOMClient.hydrateRoot(container, <App />, {
|
||||
ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
|
||||
onRecoverableError(error) {
|
||||
Scheduler.log('Log recoverable error: ' + error.message);
|
||||
},
|
||||
@@ -3435,9 +3433,6 @@ describe('ReactDOMServerPartialHydration', () => {
|
||||
{withoutStack: 1},
|
||||
);
|
||||
assertLog([
|
||||
'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
|
||||
// TODO: There were multiple mismatches in a single container. Should
|
||||
// we attempt to de-dupe them?
|
||||
'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
|
||||
'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
|
||||
]);
|
||||
|
||||
+4
-2
@@ -227,6 +227,8 @@ import {
|
||||
resetHydrationState,
|
||||
claimHydratableSingleton,
|
||||
tryToClaimNextHydratableInstance,
|
||||
tryToClaimNextHydratableTextInstance,
|
||||
tryToClaimNextHydratableSuspenseInstance,
|
||||
warnIfHydrating,
|
||||
queueHydrationError,
|
||||
} from './ReactFiberHydrationContext';
|
||||
@@ -1694,7 +1696,7 @@ function updateHostSingleton(
|
||||
|
||||
function updateHostText(current: null | Fiber, workInProgress: Fiber) {
|
||||
if (current === null) {
|
||||
tryToClaimNextHydratableInstance(workInProgress);
|
||||
tryToClaimNextHydratableTextInstance(workInProgress);
|
||||
}
|
||||
// Nothing to do here. This is terminal. We'll do the completion step
|
||||
// immediately after.
|
||||
@@ -2250,7 +2252,7 @@ function updateSuspenseComponent(
|
||||
} else {
|
||||
pushFallbackTreeSuspenseHandler(workInProgress);
|
||||
}
|
||||
tryToClaimNextHydratableInstance(workInProgress);
|
||||
tryToClaimNextHydratableSuspenseInstance(workInProgress);
|
||||
// This could've been a dehydrated suspense component.
|
||||
const suspenseState: null | SuspenseState = workInProgress.memoizedState;
|
||||
if (suspenseState !== null) {
|
||||
|
||||
@@ -21,10 +21,8 @@ function shim(...args: any): empty {
|
||||
// Hydration (when unsupported)
|
||||
export type SuspenseInstance = mixed;
|
||||
export const supportsHydration = false;
|
||||
export const isHydratable = shim;
|
||||
export const canHydrateInstance = shim;
|
||||
export const canHydrateTextInstance = shim;
|
||||
export const canHydrateSuspenseInstance = shim;
|
||||
export const isHydratableType = shim;
|
||||
export const isHydratableText = shim;
|
||||
export const isSuspenseInstancePending = shim;
|
||||
export const isSuspenseInstanceFallback = shim;
|
||||
export const getSuspenseInstanceFallbackErrorDetails = shim;
|
||||
@@ -33,6 +31,12 @@ export const getNextHydratableSibling = shim;
|
||||
export const getFirstHydratableChild = shim;
|
||||
export const getFirstHydratableChildWithinContainer = shim;
|
||||
export const getFirstHydratableChildWithinSuspenseInstance = shim;
|
||||
export const shouldSkipHydratableForInstance = shim;
|
||||
export const shouldSkipHydratableForTextInstance = shim;
|
||||
export const shouldSkipHydratableForSuspenseInstance = shim;
|
||||
export const canHydrateInstance = shim;
|
||||
export const canHydrateTextInstance = shim;
|
||||
export const canHydrateSuspenseInstance = shim;
|
||||
export const hydrateInstance = shim;
|
||||
export const hydrateTextInstance = shim;
|
||||
export const hydrateSuspenseInstance = shim;
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ function getRootHostContainer(): Container {
|
||||
return rootInstance;
|
||||
}
|
||||
|
||||
function pushHostContainer(fiber: Fiber, nextRootInstance: Container) {
|
||||
function pushHostContainer(fiber: Fiber, nextRootInstance: Container): void {
|
||||
// Push current root instance onto the stack;
|
||||
// This allows us to reset root when portals are popped.
|
||||
push(rootInstanceStackCursor, nextRootInstance, fiber);
|
||||
|
||||
+271
-82
@@ -45,10 +45,6 @@ import {
|
||||
shouldSetTextContent,
|
||||
supportsHydration,
|
||||
supportsSingletons,
|
||||
isHydratable,
|
||||
canHydrateInstance,
|
||||
canHydrateTextInstance,
|
||||
canHydrateSuspenseInstance,
|
||||
getNextHydratableSibling,
|
||||
getFirstHydratableChild,
|
||||
getFirstHydratableChildWithinContainer,
|
||||
@@ -73,6 +69,14 @@ import {
|
||||
didNotFindHydratableTextInstance,
|
||||
didNotFindHydratableSuspenseInstance,
|
||||
resolveSingletonInstance,
|
||||
shouldSkipHydratableForInstance,
|
||||
shouldSkipHydratableForTextInstance,
|
||||
shouldSkipHydratableForSuspenseInstance,
|
||||
canHydrateInstance,
|
||||
canHydrateTextInstance,
|
||||
canHydrateSuspenseInstance,
|
||||
isHydratableType,
|
||||
isHydratableText,
|
||||
} from './ReactFiberHostConfig';
|
||||
import {OffscreenLane} from './ReactFiberLane';
|
||||
import {
|
||||
@@ -95,6 +99,8 @@ let didSuspendOrErrorDEV: boolean = false;
|
||||
// Hydration errors that were thrown inside this boundary
|
||||
let hydrationErrors: Array<CapturedValue<mixed>> | null = null;
|
||||
|
||||
let rootOrSingletonContext = false;
|
||||
|
||||
function warnIfHydrating() {
|
||||
if (__DEV__) {
|
||||
if (isHydrating) {
|
||||
@@ -130,6 +136,7 @@ function enterHydrationState(fiber: Fiber): boolean {
|
||||
isHydrating = true;
|
||||
hydrationErrors = null;
|
||||
didSuspendOrErrorDEV = false;
|
||||
rootOrSingletonContext = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -147,6 +154,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
|
||||
isHydrating = true;
|
||||
hydrationErrors = null;
|
||||
didSuspendOrErrorDEV = false;
|
||||
rootOrSingletonContext = false;
|
||||
if (treeContext !== null) {
|
||||
restoreSuspendedTreeContext(fiber, treeContext);
|
||||
}
|
||||
@@ -336,63 +344,62 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
|
||||
warnNonhydratedInstance(returnFiber, fiber);
|
||||
}
|
||||
|
||||
function tryHydrate(fiber: Fiber, nextInstance: any) {
|
||||
switch (fiber.tag) {
|
||||
// HostSingleton is intentionally omitted. the hydration pathway for singletons is non-fallible
|
||||
// you can find it inlined in claimHydratableSingleton
|
||||
case HostComponent: {
|
||||
const type = fiber.type;
|
||||
const props = fiber.pendingProps;
|
||||
const instance = canHydrateInstance(nextInstance, type, props);
|
||||
if (instance !== null) {
|
||||
fiber.stateNode = (instance: Instance);
|
||||
hydrationParentFiber = fiber;
|
||||
nextHydratableInstance = getFirstHydratableChild(instance);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case HostText: {
|
||||
const text = fiber.pendingProps;
|
||||
const textInstance = canHydrateTextInstance(nextInstance, text);
|
||||
if (textInstance !== null) {
|
||||
fiber.stateNode = (textInstance: TextInstance);
|
||||
hydrationParentFiber = fiber;
|
||||
// Text Instances don't have children so there's nothing to hydrate.
|
||||
nextHydratableInstance = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case SuspenseComponent: {
|
||||
const suspenseInstance: null | SuspenseInstance =
|
||||
canHydrateSuspenseInstance(nextInstance);
|
||||
if (suspenseInstance !== null) {
|
||||
const suspenseState: SuspenseState = {
|
||||
dehydrated: suspenseInstance,
|
||||
treeContext: getSuspendedTreeContext(),
|
||||
retryLane: OffscreenLane,
|
||||
};
|
||||
fiber.memoizedState = suspenseState;
|
||||
// Store the dehydrated fragment as a child fiber.
|
||||
// This simplifies the code for getHostSibling and deleting nodes,
|
||||
// since it doesn't have to consider all Suspense boundaries and
|
||||
// check if they're dehydrated ones or not.
|
||||
const dehydratedFragment =
|
||||
createFiberFromDehydratedFragment(suspenseInstance);
|
||||
dehydratedFragment.return = fiber;
|
||||
fiber.child = dehydratedFragment;
|
||||
hydrationParentFiber = fiber;
|
||||
// While a Suspense Instance does have children, we won't step into
|
||||
// it during the first pass. Instead, we'll reenter it later.
|
||||
nextHydratableInstance = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
function tryHydrateInstance(fiber: Fiber, nextInstance: any) {
|
||||
// fiber is a HostComponent Fiber
|
||||
const instance = canHydrateInstance(
|
||||
nextInstance,
|
||||
fiber.type,
|
||||
fiber.pendingProps,
|
||||
);
|
||||
if (instance !== null) {
|
||||
fiber.stateNode = (instance: Instance);
|
||||
hydrationParentFiber = fiber;
|
||||
nextHydratableInstance = getFirstHydratableChild(instance);
|
||||
rootOrSingletonContext = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function tryHydrateText(fiber: Fiber, nextInstance: any) {
|
||||
// fiber is a HostText Fiber
|
||||
const text = fiber.pendingProps;
|
||||
const textInstance = canHydrateTextInstance(nextInstance, text);
|
||||
if (textInstance !== null) {
|
||||
fiber.stateNode = (textInstance: TextInstance);
|
||||
hydrationParentFiber = fiber;
|
||||
// Text Instances don't have children so there's nothing to hydrate.
|
||||
nextHydratableInstance = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function tryHydrateSuspense(fiber: Fiber, nextInstance: any) {
|
||||
// fiber is a SuspenseComponent Fiber
|
||||
const suspenseInstance = canHydrateSuspenseInstance(nextInstance);
|
||||
if (suspenseInstance !== null) {
|
||||
const suspenseState: SuspenseState = {
|
||||
dehydrated: suspenseInstance,
|
||||
treeContext: getSuspendedTreeContext(),
|
||||
retryLane: OffscreenLane,
|
||||
};
|
||||
fiber.memoizedState = suspenseState;
|
||||
// Store the dehydrated fragment as a child fiber.
|
||||
// This simplifies the code for getHostSibling and deleting nodes,
|
||||
// since it doesn't have to consider all Suspense boundaries and
|
||||
// check if they're dehydrated ones or not.
|
||||
const dehydratedFragment =
|
||||
createFiberFromDehydratedFragment(suspenseInstance);
|
||||
dehydratedFragment.return = fiber;
|
||||
fiber.child = dehydratedFragment;
|
||||
hydrationParentFiber = fiber;
|
||||
// While a Suspense Instance does have children, we won't step into
|
||||
// it during the first pass. Instead, we'll reenter it later.
|
||||
nextHydratableInstance = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldClientRenderOnMismatch(fiber: Fiber) {
|
||||
@@ -424,22 +431,68 @@ function claimHydratableSingleton(fiber: Fiber): void {
|
||||
false,
|
||||
));
|
||||
hydrationParentFiber = fiber;
|
||||
rootOrSingletonContext = true;
|
||||
nextHydratableInstance = getFirstHydratableChild(instance);
|
||||
}
|
||||
}
|
||||
|
||||
function advanceToFirstAttempableInstance(fiber: Fiber) {
|
||||
// fiber is HostComponent Fiber
|
||||
while (
|
||||
nextHydratableInstance &&
|
||||
shouldSkipHydratableForInstance(
|
||||
nextHydratableInstance,
|
||||
fiber.type,
|
||||
fiber.pendingProps,
|
||||
)
|
||||
) {
|
||||
// Flow doesn't understand that inside this block nextHydratableInstance is not null
|
||||
const instance: HydratableInstance = (nextHydratableInstance: any);
|
||||
nextHydratableInstance = getNextHydratableSibling(instance);
|
||||
}
|
||||
}
|
||||
|
||||
function advanceToFirstAttempableTextInstance() {
|
||||
while (
|
||||
nextHydratableInstance &&
|
||||
shouldSkipHydratableForTextInstance(nextHydratableInstance)
|
||||
) {
|
||||
// Flow doesn't understand that inside this block nextHydratableInstance is not null
|
||||
const instance: HydratableInstance = (nextHydratableInstance: any);
|
||||
nextHydratableInstance = getNextHydratableSibling(instance);
|
||||
}
|
||||
}
|
||||
|
||||
function advanceToFirstAttempableSuspenseInstance() {
|
||||
while (
|
||||
nextHydratableInstance &&
|
||||
shouldSkipHydratableForSuspenseInstance(nextHydratableInstance)
|
||||
) {
|
||||
// Flow doesn't understand that inside this block nextHydratableInstance is not null
|
||||
const instance: HydratableInstance = (nextHydratableInstance: any);
|
||||
nextHydratableInstance = getNextHydratableSibling(instance);
|
||||
}
|
||||
}
|
||||
|
||||
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
|
||||
if (!isHydrating) {
|
||||
return;
|
||||
}
|
||||
if (enableFloat && !isHydratable(fiber.type, fiber.pendingProps)) {
|
||||
// This fiber never hydrates from the DOM and always does an insert
|
||||
fiber.flags = (fiber.flags & ~Hydrating) | Placement;
|
||||
isHydrating = false;
|
||||
hydrationParentFiber = fiber;
|
||||
return;
|
||||
if (enableFloat) {
|
||||
if (!isHydratableType(fiber.type, fiber.pendingProps)) {
|
||||
// This fiber never hydrates from the DOM and always does an insert
|
||||
fiber.flags = (fiber.flags & ~Hydrating) | Placement;
|
||||
isHydrating = false;
|
||||
hydrationParentFiber = fiber;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let nextInstance = nextHydratableInstance;
|
||||
const initialInstance = nextHydratableInstance;
|
||||
if (rootOrSingletonContext) {
|
||||
// We may need to skip past certain nodes in these contexts
|
||||
advanceToFirstAttempableInstance(fiber);
|
||||
}
|
||||
const nextInstance = nextHydratableInstance;
|
||||
if (!nextInstance) {
|
||||
if (shouldClientRenderOnMismatch(fiber)) {
|
||||
warnNonhydratedInstance((hydrationParentFiber: any), fiber);
|
||||
@@ -449,10 +502,11 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
|
||||
insertNonHydratedInstance((hydrationParentFiber: any), fiber);
|
||||
isHydrating = false;
|
||||
hydrationParentFiber = fiber;
|
||||
nextHydratableInstance = initialInstance;
|
||||
return;
|
||||
}
|
||||
const firstAttemptedInstance = nextInstance;
|
||||
if (!tryHydrate(fiber, nextInstance)) {
|
||||
if (!tryHydrateInstance(fiber, nextInstance)) {
|
||||
if (shouldClientRenderOnMismatch(fiber)) {
|
||||
warnNonhydratedInstance((hydrationParentFiber: any), fiber);
|
||||
throwOnHydrationMismatch(fiber);
|
||||
@@ -460,13 +514,144 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
|
||||
// If we can't hydrate this instance let's try the next one.
|
||||
// We use this as a heuristic. It's based on intuition and not data so it
|
||||
// might be flawed or unnecessary.
|
||||
nextInstance = getNextHydratableSibling(firstAttemptedInstance);
|
||||
nextHydratableInstance = getNextHydratableSibling(nextInstance);
|
||||
const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
|
||||
if (!nextInstance || !tryHydrate(fiber, nextInstance)) {
|
||||
if (rootOrSingletonContext) {
|
||||
// We may need to skip past certain nodes in these contexts
|
||||
advanceToFirstAttempableInstance(fiber);
|
||||
}
|
||||
if (
|
||||
!nextHydratableInstance ||
|
||||
!tryHydrateInstance(fiber, nextHydratableInstance)
|
||||
) {
|
||||
// Nothing to hydrate. Make it an insertion.
|
||||
insertNonHydratedInstance((hydrationParentFiber: any), fiber);
|
||||
isHydrating = false;
|
||||
hydrationParentFiber = fiber;
|
||||
nextHydratableInstance = initialInstance;
|
||||
return;
|
||||
}
|
||||
// We matched the next one, we'll now assume that the first one was
|
||||
// superfluous and we'll delete it. Since we can't eagerly delete it
|
||||
// we'll have to schedule a deletion. To do that, this node needs a dummy
|
||||
// fiber associated with it.
|
||||
deleteHydratableInstance(prevHydrationParentFiber, firstAttemptedInstance);
|
||||
}
|
||||
}
|
||||
|
||||
function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
|
||||
if (!isHydrating) {
|
||||
return;
|
||||
}
|
||||
const text = fiber.pendingProps;
|
||||
const isHydratable = isHydratableText(text);
|
||||
|
||||
const initialInstance = nextHydratableInstance;
|
||||
if (rootOrSingletonContext && isHydratable) {
|
||||
// We may need to skip past certain nodes in these contexts.
|
||||
// We don't skip if the text is not hydratable because we know no hydratables
|
||||
// exist which could match this Fiber
|
||||
advanceToFirstAttempableTextInstance();
|
||||
}
|
||||
const nextInstance = nextHydratableInstance;
|
||||
if (!nextInstance || !isHydratable) {
|
||||
// We exclude non hydrabable text because we know there are no matching hydratables.
|
||||
// We either throw or insert depending on the render mode.
|
||||
if (shouldClientRenderOnMismatch(fiber)) {
|
||||
warnNonhydratedInstance((hydrationParentFiber: any), fiber);
|
||||
throwOnHydrationMismatch(fiber);
|
||||
}
|
||||
// Nothing to hydrate. Make it an insertion.
|
||||
insertNonHydratedInstance((hydrationParentFiber: any), fiber);
|
||||
isHydrating = false;
|
||||
hydrationParentFiber = fiber;
|
||||
nextHydratableInstance = initialInstance;
|
||||
return;
|
||||
}
|
||||
const firstAttemptedInstance = nextInstance;
|
||||
if (!tryHydrateText(fiber, nextInstance)) {
|
||||
if (shouldClientRenderOnMismatch(fiber)) {
|
||||
warnNonhydratedInstance((hydrationParentFiber: any), fiber);
|
||||
throwOnHydrationMismatch(fiber);
|
||||
}
|
||||
// If we can't hydrate this instance let's try the next one.
|
||||
// We use this as a heuristic. It's based on intuition and not data so it
|
||||
// might be flawed or unnecessary.
|
||||
nextHydratableInstance = getNextHydratableSibling(nextInstance);
|
||||
const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
|
||||
|
||||
if (rootOrSingletonContext && isHydratable) {
|
||||
// We may need to skip past certain nodes in these contexts
|
||||
advanceToFirstAttempableTextInstance();
|
||||
}
|
||||
|
||||
if (
|
||||
!nextHydratableInstance ||
|
||||
!tryHydrateText(fiber, nextHydratableInstance)
|
||||
) {
|
||||
// Nothing to hydrate. Make it an insertion.
|
||||
insertNonHydratedInstance((hydrationParentFiber: any), fiber);
|
||||
isHydrating = false;
|
||||
hydrationParentFiber = fiber;
|
||||
nextHydratableInstance = initialInstance;
|
||||
return;
|
||||
}
|
||||
// We matched the next one, we'll now assume that the first one was
|
||||
// superfluous and we'll delete it. Since we can't eagerly delete it
|
||||
// we'll have to schedule a deletion. To do that, this node needs a dummy
|
||||
// fiber associated with it.
|
||||
deleteHydratableInstance(prevHydrationParentFiber, firstAttemptedInstance);
|
||||
}
|
||||
}
|
||||
|
||||
function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
|
||||
if (!isHydrating) {
|
||||
return;
|
||||
}
|
||||
const initialInstance = nextHydratableInstance;
|
||||
if (rootOrSingletonContext) {
|
||||
// We may need to skip past certain nodes in these contexts
|
||||
advanceToFirstAttempableSuspenseInstance();
|
||||
}
|
||||
const nextInstance = nextHydratableInstance;
|
||||
if (!nextInstance) {
|
||||
if (shouldClientRenderOnMismatch(fiber)) {
|
||||
warnNonhydratedInstance((hydrationParentFiber: any), fiber);
|
||||
throwOnHydrationMismatch(fiber);
|
||||
}
|
||||
// Nothing to hydrate. Make it an insertion.
|
||||
insertNonHydratedInstance((hydrationParentFiber: any), fiber);
|
||||
isHydrating = false;
|
||||
hydrationParentFiber = fiber;
|
||||
nextHydratableInstance = initialInstance;
|
||||
return;
|
||||
}
|
||||
const firstAttemptedInstance = nextInstance;
|
||||
if (!tryHydrateSuspense(fiber, nextInstance)) {
|
||||
if (shouldClientRenderOnMismatch(fiber)) {
|
||||
warnNonhydratedInstance((hydrationParentFiber: any), fiber);
|
||||
throwOnHydrationMismatch(fiber);
|
||||
}
|
||||
// If we can't hydrate this instance let's try the next one.
|
||||
// We use this as a heuristic. It's based on intuition and not data so it
|
||||
// might be flawed or unnecessary.
|
||||
nextHydratableInstance = getNextHydratableSibling(nextInstance);
|
||||
const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
|
||||
|
||||
if (rootOrSingletonContext) {
|
||||
// We may need to skip past certain nodes in these contexts
|
||||
advanceToFirstAttempableSuspenseInstance();
|
||||
}
|
||||
|
||||
if (
|
||||
!nextHydratableInstance ||
|
||||
!tryHydrateSuspense(fiber, nextHydratableInstance)
|
||||
) {
|
||||
// Nothing to hydrate. Make it an insertion.
|
||||
insertNonHydratedInstance((hydrationParentFiber: any), fiber);
|
||||
isHydrating = false;
|
||||
hydrationParentFiber = fiber;
|
||||
nextHydratableInstance = initialInstance;
|
||||
return;
|
||||
}
|
||||
// We matched the next one, we'll now assume that the first one was
|
||||
@@ -616,19 +801,21 @@ function skipPastDehydratedSuspenseInstance(
|
||||
}
|
||||
|
||||
function popToNextHostParent(fiber: Fiber): void {
|
||||
let parent = fiber.return;
|
||||
while (
|
||||
parent !== null &&
|
||||
parent.tag !== HostComponent &&
|
||||
parent.tag !== HostRoot &&
|
||||
parent.tag !== SuspenseComponent &&
|
||||
(!(enableHostSingletons && supportsSingletons)
|
||||
? true
|
||||
: parent.tag !== HostSingleton)
|
||||
) {
|
||||
parent = parent.return;
|
||||
hydrationParentFiber = fiber.return;
|
||||
while (hydrationParentFiber) {
|
||||
switch (hydrationParentFiber.tag) {
|
||||
case HostRoot:
|
||||
case HostSingleton:
|
||||
rootOrSingletonContext = true;
|
||||
return;
|
||||
case HostComponent:
|
||||
case SuspenseComponent:
|
||||
rootOrSingletonContext = false;
|
||||
return;
|
||||
default:
|
||||
hydrationParentFiber = hydrationParentFiber.return;
|
||||
}
|
||||
}
|
||||
hydrationParentFiber = parent;
|
||||
}
|
||||
|
||||
function popHydrationState(fiber: Fiber): boolean {
|
||||
@@ -755,6 +942,8 @@ export {
|
||||
resetHydrationState,
|
||||
claimHydratableSingleton,
|
||||
tryToClaimNextHydratableInstance,
|
||||
tryToClaimNextHydratableTextInstance,
|
||||
tryToClaimNextHydratableSuspenseInstance,
|
||||
prepareToHydrateHostInstance,
|
||||
prepareToHydrateHostTextInstance,
|
||||
prepareToHydrateHostSuspenseInstance,
|
||||
|
||||
@@ -130,11 +130,8 @@ export const cloneHiddenTextInstance = $$$hostConfig.cloneHiddenTextInstance;
|
||||
// Hydration
|
||||
// (optional)
|
||||
// -------------------
|
||||
export const isHydratable = $$$hostConfig.isHydratable;
|
||||
export const canHydrateInstance = $$$hostConfig.canHydrateInstance;
|
||||
export const canHydrateTextInstance = $$$hostConfig.canHydrateTextInstance;
|
||||
export const canHydrateSuspenseInstance =
|
||||
$$$hostConfig.canHydrateSuspenseInstance;
|
||||
export const isHydratableType = $$$hostConfig.isHydratableType;
|
||||
export const isHydratableText = $$$hostConfig.isHydratableText;
|
||||
export const isSuspenseInstancePending =
|
||||
$$$hostConfig.isSuspenseInstancePending;
|
||||
export const isSuspenseInstanceFallback =
|
||||
@@ -149,6 +146,16 @@ export const getFirstHydratableChildWithinContainer =
|
||||
$$$hostConfig.getFirstHydratableChildWithinContainer;
|
||||
export const getFirstHydratableChildWithinSuspenseInstance =
|
||||
$$$hostConfig.getFirstHydratableChildWithinSuspenseInstance;
|
||||
export const shouldSkipHydratableForInstance =
|
||||
$$$hostConfig.shouldSkipHydratableForInstance;
|
||||
export const shouldSkipHydratableForTextInstance =
|
||||
$$$hostConfig.shouldSkipHydratableForTextInstance;
|
||||
export const shouldSkipHydratableForSuspenseInstance =
|
||||
$$$hostConfig.shouldSkipHydratableForSuspenseInstance;
|
||||
export const canHydrateInstance = $$$hostConfig.canHydrateInstance;
|
||||
export const canHydrateTextInstance = $$$hostConfig.canHydrateTextInstance;
|
||||
export const canHydrateSuspenseInstance =
|
||||
$$$hostConfig.canHydrateSuspenseInstance;
|
||||
export const hydrateInstance = $$$hostConfig.hydrateInstance;
|
||||
export const hydrateTextInstance = $$$hostConfig.hydrateTextInstance;
|
||||
export const hydrateSuspenseInstance = $$$hostConfig.hydrateSuspenseInstance;
|
||||
|
||||
Reference in New Issue
Block a user