@samitouri / QOS-React-2 / commits / 8a9f82ed58

[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

Josh Story committed Mar 6, 2023 at 15:00 UTC 8a9f82ed58c2fa76583a041fa34ad80f5f94a3d1
12 files changed +983 -398
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+94 -89
@@ -55,7 +55,12 @@ import {
55 setValueForStyles,
56 validateShorthandPropertyCollisionInDev,
57 } from './CSSPropertyOperations';
58 -import {HTML_NAMESPACE, getIntrinsicNamespace} from '../shared/DOMNamespaces';
58 +import {
59 + HTML_NAMESPACE,
60 + MATH_NAMESPACE,
61 + SVG_NAMESPACE,
62 + getIntrinsicNamespace,
63 +} from '../shared/DOMNamespaces';
64 import {
65 getPropertyInfo,
66 shouldIgnoreAttribute,
@@ -375,112 +380,112 @@ function updateDOMProperties(
380 }
381 }
382
378 -export function createElement(
383 +// Creates elements in the HTML namesapce
384 +export function createHTMLElement(
385 type: string,
386 props: Object,
381 - rootContainerElement: Element | Document | DocumentFragment,
382 - parentNamespace: string,
387 + ownerDocument: Document,
388 ): Element {
389 let isCustomComponentTag;
390
386 - // We create tags in the namespace of their parent container, except HTML
387 - // tags get no namespace.
388 - const ownerDocument: Document =
389 - getOwnerDocumentFromRootContainer(rootContainerElement);
391 let domElement: Element;
391 - let namespaceURI = parentNamespace;
392 - if (namespaceURI === HTML_NAMESPACE) {
393 - namespaceURI = getIntrinsicNamespace(type);
392 + if (__DEV__) {
393 + isCustomComponentTag = isCustomComponent(type, props);
394 + // Should this check be gated by parent namespace? Not sure we want to
395 + // allow <SVG> or <mATH>.
396 + if (!isCustomComponentTag && type !== type.toLowerCase()) {
397 + console.error(
398 + '<%s /> is using incorrect casing. ' +
399 + 'Use PascalCase for React components, ' +
400 + 'or lowercase for HTML elements.',
401 + type,
402 + );
403 + }
404 }
395 - if (namespaceURI === HTML_NAMESPACE) {
405 +
406 + if (type === 'script') {
407 + // Create the script via .innerHTML so its "parser-inserted" flag is
408 + // set to true and it does not execute
409 + const div = ownerDocument.createElement('div');
410 if (__DEV__) {
397 - isCustomComponentTag = isCustomComponent(type, props);
398 - // Should this check be gated by parent namespace? Not sure we want to
399 - // allow <SVG> or <mATH>.
400 - if (!isCustomComponentTag && type !== type.toLowerCase()) {
411 + if (enableTrustedTypesIntegration && !didWarnScriptTags) {
412 console.error(
402 - '<%s /> is using incorrect casing. ' +
403 - 'Use PascalCase for React components, ' +
404 - 'or lowercase for HTML elements.',
405 - type,
413 + 'Encountered a script tag while rendering React component. ' +
414 + 'Scripts inside React components are never executed when rendering ' +
415 + 'on the client. Consider using template tag instead ' +
416 + '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).',
417 );
418 + didWarnScriptTags = true;
419 }
420 }
409 -
410 - if (type === 'script') {
411 - // Create the script via .innerHTML so its "parser-inserted" flag is
412 - // set to true and it does not execute
413 - const div = ownerDocument.createElement('div');
414 - if (__DEV__) {
415 - if (enableTrustedTypesIntegration && !didWarnScriptTags) {
416 - console.error(
417 - 'Encountered a script tag while rendering React component. ' +
418 - 'Scripts inside React components are never executed when rendering ' +
419 - 'on the client. Consider using template tag instead ' +
420 - '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).',
421 - );
422 - didWarnScriptTags = true;
423 - }
424 - }
425 - div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
426 - // This is guaranteed to yield a script element.
427 - const firstChild = ((div.firstChild: any): HTMLScriptElement);
428 - domElement = div.removeChild(firstChild);
429 - } else if (typeof props.is === 'string') {
430 - domElement = ownerDocument.createElement(type, {is: props.is});
431 - } else {
432 - // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
433 - // See discussion in https://github.com/facebook/react/pull/6896
434 - // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
435 - domElement = ownerDocument.createElement(type);
436 - // Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` and `size`
437 - // attributes on `select`s needs to be added before `option`s are inserted.
438 - // This prevents:
439 - // - a bug where the `select` does not scroll to the correct option because singular
440 - // `select` elements automatically pick the first item #13222
441 - // - a bug where the `select` set the first item as selected despite the `size` attribute #14239
442 - // See https://github.com/facebook/react/issues/13222
443 - // and https://github.com/facebook/react/issues/14239
444 - if (type === 'select') {
445 - const node = ((domElement: any): HTMLSelectElement);
446 - if (props.multiple) {
447 - node.multiple = true;
448 - } else if (props.size) {
449 - // Setting a size greater than 1 causes a select to behave like `multiple=true`, where
450 - // it is possible that no option is selected.
451 - //
452 - // This is only necessary when a select in "single selection mode".
453 - node.size = props.size;
454 - }
421 + div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
422 + // This is guaranteed to yield a script element.
423 + const firstChild = ((div.firstChild: any): HTMLScriptElement);
424 + domElement = div.removeChild(firstChild);
425 + } else if (typeof props.is === 'string') {
426 + domElement = ownerDocument.createElement(type, {is: props.is});
427 + } else {
428 + // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
429 + // See discussion in https://github.com/facebook/react/pull/6896
430 + // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
431 + domElement = ownerDocument.createElement(type);
432 + // Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` and `size`
433 + // attributes on `select`s needs to be added before `option`s are inserted.
434 + // This prevents:
435 + // - a bug where the `select` does not scroll to the correct option because singular
436 + // `select` elements automatically pick the first item #13222
437 + // - a bug where the `select` set the first item as selected despite the `size` attribute #14239
438 + // See https://github.com/facebook/react/issues/13222
439 + // and https://github.com/facebook/react/issues/14239
440 + if (type === 'select') {
441 + const node = ((domElement: any): HTMLSelectElement);
442 + if (props.multiple) {
443 + node.multiple = true;
444 + } else if (props.size) {
445 + // Setting a size greater than 1 causes a select to behave like `multiple=true`, where
446 + // it is possible that no option is selected.
447 + //
448 + // This is only necessary when a select in "single selection mode".
449 + node.size = props.size;
450 }
451 }
457 - } else {
458 - domElement = ownerDocument.createElementNS(namespaceURI, type);
452 }
453
454 if (__DEV__) {
462 - if (namespaceURI === HTML_NAMESPACE) {
463 - if (
464 - !isCustomComponentTag &&
465 - // $FlowFixMe[method-unbinding]
466 - Object.prototype.toString.call(domElement) ===
467 - '[object HTMLUnknownElement]' &&
468 - !hasOwnProperty.call(warnedUnknownTags, type)
469 - ) {
470 - warnedUnknownTags[type] = true;
471 - console.error(
472 - 'The tag <%s> is unrecognized in this browser. ' +
473 - 'If you meant to render a React component, start its name with ' +
474 - 'an uppercase letter.',
475 - type,
476 - );
477 - }
455 + if (
456 + !isCustomComponentTag &&
457 + // $FlowFixMe[method-unbinding]
458 + Object.prototype.toString.call(domElement) ===
459 + '[object HTMLUnknownElement]' &&
460 + !hasOwnProperty.call(warnedUnknownTags, type)
461 + ) {
462 + warnedUnknownTags[type] = true;
463 + console.error(
464 + 'The tag <%s> is unrecognized in this browser. ' +
465 + 'If you meant to render a React component, start its name with ' +
466 + 'an uppercase letter.',
467 + type,
468 + );
469 }
470 }
471
472 return domElement;
473 }
474
475 +export function createSVGElement(
476 + type: string,
477 + ownerDocument: Document,
478 +): Element {
479 + return ownerDocument.createElementNS(SVG_NAMESPACE, type);
480 +}
481 +
482 +export function createMathElement(
483 + type: string,
484 + ownerDocument: Document,
485 +): Element {
486 + return ownerDocument.createElementNS(MATH_NAMESPACE, type);
487 +}
488 +
489 export function createTextNode(
490 text: string,
491 rootContainerElement: Element | Document | DocumentFragment,
@@ -864,9 +869,9 @@ export function diffHydratedProperties(
869 domElement: Element,
870 tag: string,
871 rawProps: Object,
867 - parentNamespace: string,
872 isConcurrentMode: boolean,
873 shouldWarnDev: boolean,
874 + parentNamespaceDev: string,
875 ): null | Array<mixed> {
876 let isCustomComponentTag;
877 let extraAttributeNames: Set<string>;
@@ -1109,11 +1114,11 @@ export function diffHydratedProperties(
1114 propertyInfo,
1115 );
1116 } else {
1112 - let ownNamespace = parentNamespace;
1113 - if (ownNamespace === HTML_NAMESPACE) {
1114 - ownNamespace = getIntrinsicNamespace(tag);
1117 + let ownNamespaceDev = parentNamespaceDev;
1118 + if (ownNamespaceDev === HTML_NAMESPACE) {
1119 + ownNamespaceDev = getIntrinsicNamespace(tag);
1120 }
1116 - if (ownNamespace === HTML_NAMESPACE) {
1121 + if (ownNamespaceDev === HTML_NAMESPACE) {
1122 // $FlowFixMe - Should be inferred as not undefined.
1123 extraAttributeNames.delete(propKey.toLowerCase());
1124 } else {
packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
+29 -48
@@ -9,30 +9,32 @@
9
10 import type {Instance, Container} from './ReactDOMHostConfig';
11
12 -import {isAttributeNameSafe} from '../shared/DOMProperty';
13 -import {precacheFiberNode} from './ReactDOMComponentTree';
12 +import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
13
14 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
15 const {Dispatcher} = ReactDOMSharedInternals;
16 +import {
17 + checkAttributeStringCoercion,
18 + checkPropStringCoercion,
19 +} from 'shared/CheckStringCoercion';
20 +
21 import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
22 +import {isAttributeNameSafe} from '../shared/DOMProperty';
23 +import {SVG_NAMESPACE} from '../shared/DOMNamespaces';
24 import {
25 validatePreloadArguments,
26 validatePreinitArguments,
27 getValueDescriptorExpectingObjectForWarning,
28 getValueDescriptorExpectingEnumForWarning,
29 } from '../shared/ReactDOMResourceValidation';
24 -import {createElement, setInitialProperties} from './ReactDOMComponent';
25 -import {
26 - checkAttributeStringCoercion,
27 - checkPropStringCoercion,
28 -} from 'shared/CheckStringCoercion';
30 +
31 +import {precacheFiberNode} from './ReactDOMComponentTree';
32 +import {createHTMLElement, setInitialProperties} from './ReactDOMComponent';
33 import {
34 getResourcesFromRoot,
35 isMarkedResource,
36 markNodeAsResource,
37 } from './ReactDOMComponentTree';
34 -import {HTML_NAMESPACE, SVG_NAMESPACE} from '../shared/DOMNamespaces';
35 -import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
38
39 // The resource types we support. currently they match the form for the as argument.
40 // In the future this may need to change, especially when modules / scripts are supported
@@ -174,11 +176,10 @@ function preconnectAs(
176
177 const preconnectProps = {rel, crossOrigin, href};
178 if (null === ownerDocument.querySelector(key)) {
177 - const preloadInstance = createElement(
179 + const preloadInstance = createHTMLElement(
180 'link',
181 preconnectProps,
182 ownerDocument,
181 - HTML_NAMESPACE,
183 );
184 setInitialProperties(preloadInstance, 'link', preconnectProps);
185 markNodeAsResource(preloadInstance);
@@ -289,11 +290,10 @@ function preload(href: string, options: PreloadOptions) {
290 preloadPropsMap.set(key, preloadProps);
291
292 if (null === ownerDocument.querySelector(preloadKey)) {
292 - const preloadInstance = createElement(
293 + const preloadInstance = createHTMLElement(
294 'link',
295 preloadProps,
296 ownerDocument,
296 - HTML_NAMESPACE,
297 );
298 setInitialProperties(preloadInstance, 'link', preloadProps);
299 markNodeAsResource(preloadInstance);
@@ -371,11 +371,10 @@ function preinit(href: string, options: PreinitOptions) {
371 preloadPropsMap.set(key, preloadProps);
372
373 if (null === preloadDocument.querySelector(preloadKey)) {
374 - const preloadInstance = createElement(
374 + const preloadInstance = createHTMLElement(
375 'link',
376 preloadProps,
377 preloadDocument,
378 - HTML_NAMESPACE,
378 );
379 setInitialProperties(preloadInstance, 'link', preloadProps);
380 markNodeAsResource(preloadInstance);
@@ -417,12 +416,8 @@ function preinit(href: string, options: PreinitOptions) {
416 if (preloadProps) {
417 adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
418 }
420 - instance = createElement(
421 - 'link',
422 - stylesheetProps,
423 - resourceRoot,
424 - HTML_NAMESPACE,
425 - );
419 + const ownerDocument = getDocumentFromRoot(resourceRoot);
420 + instance = createHTMLElement('link', stylesheetProps, ownerDocument);
421 markNodeAsResource(instance);
422 setInitialProperties(instance, 'link', stylesheetProps);
423 insertStylesheet(instance, precedence, resourceRoot);
@@ -463,12 +458,8 @@ function preinit(href: string, options: PreinitOptions) {
458 if (preloadProps) {
459 adoptPreloadPropsForScript(scriptProps, preloadProps);
460 }
466 - instance = createElement(
467 - 'script',
468 - scriptProps,
469 - resourceRoot,
470 - HTML_NAMESPACE,
471 - );
461 + const ownerDocument = getDocumentFromRoot(resourceRoot);
462 + instance = createHTMLElement('script', scriptProps, ownerDocument);
463 markNodeAsResource(instance);
464 setInitialProperties(instance, 'link', scriptProps);
465 (getDocumentFromRoot(resourceRoot).head: any).appendChild(instance);
@@ -703,11 +694,10 @@ function preloadStylesheet(
694 null ===
695 ownerDocument.querySelector(getPreloadStylesheetSelectorFromKey(key))
696 ) {
706 - const preloadInstance = createElement(
697 + const preloadInstance = createHTMLElement(
698 'link',
699 preloadProps,
700 ownerDocument,
710 - HTML_NAMESPACE,
701 );
702 setInitialProperties(preloadInstance, 'link', preloadProps);
703 markNodeAsResource(preloadInstance);
@@ -762,16 +752,13 @@ export function acquireResource(
752 );
753 if (instance) {
754 resource.instance = instance;
755 + markNodeAsResource(instance);
756 return instance;
757 }
758
759 const styleProps = styleTagPropsFromRawProps(props);
769 - instance = createElement(
770 - 'style',
771 - styleProps,
772 - hoistableRoot,
773 - HTML_NAMESPACE,
774 - );
760 + const ownerDocument = getDocumentFromRoot(hoistableRoot);
761 + instance = createHTMLElement('style', styleProps, ownerDocument);
762
763 markNodeAsResource(instance);
764 setInitialProperties(instance, 'style', styleProps);
@@ -793,6 +780,7 @@ export function acquireResource(
780 );
781 if (instance) {
782 resource.instance = instance;
783 + markNodeAsResource(instance);
784 return instance;
785 }
786
@@ -803,12 +791,8 @@ export function acquireResource(
791 }
792
793 // Construct and insert a new instance
806 - instance = createElement(
807 - 'link',
808 - stylesheetProps,
809 - hoistableRoot,
810 - HTML_NAMESPACE,
811 - );
794 + const ownerDocument = getDocumentFromRoot(hoistableRoot);
795 + instance = createHTMLElement('link', stylesheetProps, ownerDocument);
796 markNodeAsResource(instance);
797 const linkInstance: HTMLLinkElement = (instance: any);
798 (linkInstance: any)._p = new Promise((resolve, reject) => {
@@ -837,6 +821,7 @@ export function acquireResource(
821 );
822 if (instance) {
823 resource.instance = instance;
824 + markNodeAsResource(instance);
825 return instance;
826 }
827
@@ -848,12 +833,8 @@ export function acquireResource(
833 }
834
835 // Construct and insert a new instance
851 - instance = createElement(
852 - 'script',
853 - scriptProps,
854 - hoistableRoot,
855 - HTML_NAMESPACE,
856 - );
836 + const ownerDocument = getDocumentFromRoot(hoistableRoot);
837 + instance = createHTMLElement('script', scriptProps, ownerDocument);
838 markNodeAsResource(instance);
839 setInitialProperties(instance, 'link', scriptProps);
840 (getDocumentFromRoot(hoistableRoot).head: any).appendChild(instance);
@@ -1092,7 +1073,7 @@ export function hydrateHoistable(
1073 }
1074
1075 // There is no matching instance to hydrate, we create it now
1095 - const instance = createElement(type, props, ownerDocument, HTML_NAMESPACE);
1076 + const instance = createHTMLElement(type, props, ownerDocument);
1077 setInitialProperties(instance, type, props);
1078 precacheFiberNode(internalInstanceHandle, instance);
1079 markNodeAsResource(instance);
packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
+211 -155
@@ -32,7 +32,9 @@ import {
32 export {detachDeletedInstance};
33 import {hasRole} from './DOMAccessibilityRoles';
34 import {
35 - createElement,
35 + createHTMLElement,
36 + createSVGElement,
37 + createMathElement,
38 createTextNode,
39 setInitialProperties,
40 diffProperties,
@@ -58,6 +60,7 @@ import {
60 import {
61 getChildNamespace,
62 SVG_NAMESPACE,
63 + MATH_NAMESPACE,
64 HTML_NAMESPACE,
65 } from '../shared/DOMNamespaces';
66 import {
@@ -144,7 +147,7 @@ export interface SuspenseInstance extends Comment {
147 export type HydratableInstance = Instance | TextInstance | SuspenseInstance;
148 export type PublicInstance = Element | Text;
149 type HostContextDev = {
147 - namespace: string,
150 + namespace: HostContextProd,
151 ancestorInfo: AncestorInfoDev,
152 };
153 type HostContextProd = string;
@@ -178,7 +181,7 @@ export function getRootHostContext(
181 rootContainerInstance: Container,
182 ): HostContext {
183 let type;
181 - let namespace;
184 + let namespace: HostContextProd;
185 const nodeType = rootContainerInstance.nodeType;
186 switch (nodeType) {
187 case DOCUMENT_NODE:
@@ -274,12 +277,10 @@ export function createHoistableInstance(
277 rootContainerInstance: Container,
278 internalInstanceHandle: Object,
279 ): Instance {
277 - const domElement: Instance = createElement(
278 - type,
279 - props,
280 + const ownerDocument = getOwnerDocumentFromRootContainer(
281 rootContainerInstance,
281 - HTML_NAMESPACE,
282 );
283 + const domElement: Instance = createHTMLElement(type, props, ownerDocument);
284 precacheFiberNode(internalInstanceHandle, domElement);
285 updateFiberProps(domElement, props);
286 setInitialProperties(domElement, type, props);
@@ -294,10 +295,10 @@ export function createInstance(
295 hostContext: HostContext,
296 internalInstanceHandle: Object,
297 ): Instance {
297 - let parentNamespace: string;
298 + let namespace;
299 if (__DEV__) {
300 // TODO: take namespace into account when validating.
300 - const hostContextDev = ((hostContext: any): HostContextDev);
301 + const hostContextDev: HostContextDev = (hostContext: any);
302 validateDOMNesting(type, null, hostContextDev.ancestorInfo);
303 if (
304 typeof props.children === 'string' ||
@@ -310,16 +311,37 @@ export function createInstance(
311 );
312 validateDOMNesting(null, string, ownAncestorInfo);
313 }
313 - parentNamespace = hostContextDev.namespace;
314 + namespace = hostContextDev.namespace;
315 } else {
315 - parentNamespace = ((hostContext: any): HostContextProd);
316 + const hostContextProd: HostContextProd = (hostContext: any);
317 + namespace = hostContextProd;
318 }
317 - const domElement: Instance = createElement(
318 - type,
319 - props,
319 +
320 + const ownerDocument = getOwnerDocumentFromRootContainer(
321 rootContainerInstance,
321 - parentNamespace,
322 );
323 +
324 + let domElement: Instance;
325 + create: switch (namespace) {
326 + case SVG_NAMESPACE:
327 + domElement = createSVGElement(type, ownerDocument);
328 + break;
329 + case MATH_NAMESPACE:
330 + domElement = createMathElement(type, ownerDocument);
331 + break;
332 + case HTML_NAMESPACE:
333 + switch (type) {
334 + case 'svg':
335 + domElement = createSVGElement(type, ownerDocument);
336 + break create;
337 + case 'math':
338 + domElement = createMathElement(type, ownerDocument);
339 + break create;
340 + }
341 + // eslint-disable-next-line no-fallthrough
342 + default:
343 + domElement = createHTMLElement(type, props, ownerDocument);
344 + }
345 precacheFiberNode(internalInstanceHandle, domElement);
346 updateFiberProps(domElement, props);
347 return domElement;
@@ -825,17 +847,9 @@ export const supportsHydration = true;
847
848 // With Resources, some HostComponent types will never be server rendered and need to be
849 // inserted without breaking hydration
828 -export function isHydratable(type: string, props: Props): boolean {
850 +export function isHydratableType(type: string, props: Props): boolean {
851 if (enableFloat) {
830 - if (type === 'link') {
831 - if (
832 - (props: any).rel === 'stylesheet' &&
833 - typeof (props: any).precedence !== 'string'
834 - ) {
835 - return true;
836 - }
837 - return false;
838 - } else if (type === 'script') {
852 + if (type === 'script') {
853 const {async, onLoad, onError} = (props: any);
854 return !(async && (onLoad || onError));
855 }
@@ -844,6 +858,129 @@ export function isHydratable(type: string, props: Props): boolean {
858 return true;
859 }
860 }
861 +export function isHydratableText(text: string): boolean {
862 + return text !== '';
863 +}
864 +
865 +export function shouldSkipHydratableForInstance(
866 + instance: HydratableInstance,
867 + type: string,
868 + props: Props,
869 +): boolean {
870 + if (instance.nodeType !== ELEMENT_NODE) {
871 + // This is a suspense boundary or Text node.
872 + // Suspense Boundaries are never expected to be injected by 3rd parties. If we see one it should be matched
873 + // and this is a hydration error.
874 + // Text Nodes are also not expected to be injected by 3rd parties. This is less of a guarantee for <body>
875 + // but it seems reasonable and conservative to reject this as a hydration error as well
876 + return false;
877 + } else if (
878 + instance.nodeName.toLowerCase() !== type.toLowerCase() ||
879 + isMarkedResource(instance)
880 + ) {
881 + // We are either about to
882 + return true;
883 + } else {
884 + // We have an Element with the right type.
885 + const element: Element = (instance: any);
886 + const anyProps = (props: any);
887 +
888 + // We are going to try to exclude it if we can definitely identify it as a hoisted Node or if
889 + // we can guess that the node is likely hoisted or was inserted by a 3rd party script or browser extension
890 + // using high entropy attributes for certain types. This technique will fail for strange insertions like
891 + // extension prepending <div> in the <body> but that already breaks before and that is an edge case.
892 + switch (type) {
893 + // case 'title':
894 + //We assume all titles are matchable. You should only have one in the Document, at least in a hoistable scope
895 + // 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.
896 + case 'meta': {
897 + // The only way to opt out of hoisting meta tags is to give it an itemprop attribute. We assume there will be
898 + // not 3rd party meta tags that are prepended, accepting the cases where this isn't true because meta tags
899 + // are usually only functional for SSR so even in a rare case where we did bind to an injected tag the runtime
900 + // implications are minimal
901 + if (!element.hasAttribute('itemprop')) {
902 + // This is a Hoistable
903 + return true;
904 + }
905 + break;
906 + }
907 + case 'link': {
908 + // Links come in many forms and we do expect 3rd parties to inject them into <head> / <body>. We exclude known resources
909 + // and then use high-entroy attributes like href which are almost always used and almost always unique to filter out unlikely
910 + // matches.
911 + const rel = element.getAttribute('rel');
912 + if (rel === 'stylesheet' && element.hasAttribute('data-precedence')) {
913 + // This is a stylesheet resource
914 + return true;
915 + } else if (
916 + rel !== anyProps.rel ||
917 + element.getAttribute('href') !==
918 + (anyProps.href == null ? null : anyProps.href) ||
919 + element.getAttribute('crossorigin') !==
920 + (anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ||
921 + element.getAttribute('title') !==
922 + (anyProps.title == null ? null : anyProps.title)
923 + ) {
924 + // rel + href should usually be enough to uniquely identify a link however crossOrigin can vary for rel preconnect
925 + // and title could vary for rel alternate
926 + return true;
927 + }
928 + break;
929 + }
930 + case 'style': {
931 + // Styles are hard to match correctly. We can exclude known resources but otherwise we accept the fact that a non-hoisted style tags
932 + // 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
933 + if (element.hasAttribute('data-precedence')) {
934 + // This is a style resource
935 + return true;
936 + }
937 + break;
938 + }
939 + case 'script': {
940 + // Scripts are a little tricky, we exclude known resources and then similar to links try to use high-entropy attributes
941 + // to reject poor matches. One challenge with scripts are inline scripts. We don't attempt to check text content which could
942 + // in theory lead to a hydration error later if a 3rd party injected an inline script before the React rendered nodes.
943 + // Falling back to client rendering if this happens should be seemless though so we will try this hueristic and revisit later
944 + // if we learn it is problematic
945 + const srcAttr = element.getAttribute('src');
946 + if (
947 + srcAttr &&
948 + element.hasAttribute('async') &&
949 + !element.hasAttribute('itemprop')
950 + ) {
951 + // This is an async script resource
952 + return true;
953 + } else if (
954 + srcAttr !== (anyProps.src == null ? null : anyProps.src) ||
955 + element.getAttribute('type') !==
956 + (anyProps.type == null ? null : anyProps.type) ||
957 + element.getAttribute('crossorigin') !==
958 + (anyProps.crossOrigin == null ? null : anyProps.crossOrigin)
959 + ) {
960 + // This script is for a different src
961 + return true;
962 + }
963 + break;
964 + }
965 + }
966 + // We have excluded the most likely cases of mismatch between hoistable tags, 3rd party script inserted tags,
967 + // and browser extension inserted tags. While it is possible this is not the right match it is a decent hueristic
968 + // that should work in the vast majority of cases.
969 + return false;
970 + }
971 +}
972 +
973 +export function shouldSkipHydratableForTextInstance(
974 + instance: HydratableInstance,
975 +): boolean {
976 + return instance.nodeType === ELEMENT_NODE;
977 +}
978 +
979 +export function shouldSkipHydratableForSuspenseInstance(
980 + instance: HydratableInstance,
981 +): boolean {
982 + return instance.nodeType === ELEMENT_NODE;
983 +}
984
985 export function canHydrateInstance(
986 instance: HydratableInstance,
@@ -852,19 +989,21 @@ export function canHydrateInstance(
989 ): null | Instance {
990 if (
991 instance.nodeType !== ELEMENT_NODE ||
855 - type.toLowerCase() !== instance.nodeName.toLowerCase()
992 + instance.nodeName.toLowerCase() !== type.toLowerCase()
993 ) {
994 return null;
995 + } else {
996 + return ((instance: any): Instance);
997 }
859 - // This has now been refined to an element node.
860 - return ((instance: any): Instance);
998 }
999
1000 export function canHydrateTextInstance(
1001 instance: HydratableInstance,
1002 text: string,
1003 ): null | TextInstance {
867 - if (text === '' || instance.nodeType !== TEXT_NODE) {
1004 + if (text === '') return null;
1005 +
1006 + if (instance.nodeType !== TEXT_NODE) {
1007 // Empty strings are not parsed by HTML so there won't be a correct match here.
1008 return null;
1009 }
@@ -876,7 +1015,6 @@ export function canHydrateSuspenseInstance(
1015 instance: HydratableInstance,
1016 ): null | SuspenseInstance {
1017 if (instance.nodeType !== COMMENT_NODE) {
879 - // Empty strings are not parsed by HTML so there won't be a correct match here.
1018 return null;
1019 }
1020 // This has now been refined to a suspense node.
@@ -931,114 +1069,8 @@ function getNextHydratable(node: ?Node) {
1069 // Skip non-hydratable nodes.
1070 for (; node != null; node = ((node: any): Node).nextSibling) {
1071 const nodeType = node.nodeType;
934 - if (enableFloat && enableHostSingletons) {
935 - if (nodeType === ELEMENT_NODE) {
936 - const element: Element = (node: any);
937 - switch (element.tagName) {
938 - // This is subtle. in SVG scope the title tag is case sensitive. we don't want to skip
939 - // titles in svg but we do want to skip them outside of svg. there is an edge case where
940 - // you could do `React.createElement('TITLE', ...)` inside an svg scope but the SSR serializer
941 - // will still emit lowercase. Practically speaking the only time the DOM will have a non-uppercased
942 - // title tagName is if it is inside an svg.
943 - // Other Resource types like META, BASE, LINK, and SCRIPT should be treated as resources even inside
944 - // svg scope because they are invalid otherwise. We still don't need to handle the lowercase variant
945 - // because if they are present in the DOM already they would have been hoisted outside the SVG scope
946 - // as Resources. So while it would be correct to skip a <link> inside <svg> and this algorithm won't
947 - // skip that link because the tagName will not be uppercased it functionally is irrelevant. If one
948 - // tries to render incompatible types such as a non-resource stylesheet inside an svg the server will
949 - // emit that invalid html and hydration will fail. In Dev this will present warnings guiding the
950 - // developer on how to fix.
951 - case 'TITLE':
952 - case 'META':
953 - case 'HTML':
954 - case 'HEAD':
955 - case 'BODY': {
956 - continue;
957 - }
958 - case 'LINK': {
959 - const linkEl: HTMLLinkElement = (element: any);
960 - // All links that are server rendered are resources except
961 - // stylesheets that do not have a precedence
962 - if (
963 - linkEl.rel === 'stylesheet' &&
964 - !linkEl.hasAttribute('data-precedence')
965 - ) {
966 - break;
967 - }
968 - continue;
969 - }
970 - case 'STYLE': {
971 - const styleEl: HTMLStyleElement = (element: any);
972 - if (styleEl.hasAttribute('data-precedence')) {
973 - continue;
974 - }
975 - break;
976 - }
977 - case 'SCRIPT': {
978 - const scriptEl: HTMLScriptElement = (element: any);
979 - if (scriptEl.hasAttribute('async')) {
980 - continue;
981 - }
982 - break;
983 - }
984 - }
985 - break;
986 - } else if (nodeType === TEXT_NODE) {
987 - break;
988 - }
989 - } else if (enableFloat) {
990 - if (nodeType === ELEMENT_NODE) {
991 - const element: Element = (node: any);
992 - switch (element.tagName) {
993 - case 'TITLE':
994 - case 'META': {
995 - continue;
996 - }
997 - case 'LINK': {
998 - const linkEl: HTMLLinkElement = (element: any);
999 - // All links that are server rendered are resources except
1000 - // stylesheets that do not have a precedence
1001 - if (
1002 - linkEl.rel === 'stylesheet' &&
1003 - !linkEl.hasAttribute('data-precedence')
1004 - ) {
1005 - break;
1006 - }
1007 - continue;
1008 - }
1009 - case 'STYLE': {
1010 - const styleEl: HTMLStyleElement = (element: any);
1011 - if (styleEl.hasAttribute('data-precedence')) {
1012 - continue;
1013 - }
1014 - break;
1015 - }
1016 - case 'SCRIPT': {
1017 - const scriptEl: HTMLScriptElement = (element: any);
1018 - if (scriptEl.hasAttribute('async')) {
1019 - continue;
1020 - }
1021 - break;
1022 - }
1023 - }
1024 - break;
1025 - } else if (nodeType === TEXT_NODE) {
1026 - break;
1027 - }
1028 - } else if (enableHostSingletons) {
1029 - if (nodeType === ELEMENT_NODE) {
1030 - const tag: string = (node: any).tagName;
1031 - if (tag === 'HTML' || tag === 'HEAD' || tag === 'BODY') {
1032 - continue;
1033 - }
1034 - break;
1035 - } else if (nodeType === TEXT_NODE) {
1036 - break;
1037 - }
1038 - } else {
1039 - if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) {
1040 - break;
1041 - }
1072 + if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) {
1073 + break;
1074 }
1075 if (nodeType === COMMENT_NODE) {
1076 const nodeData = (node: any).data;
@@ -1093,26 +1125,28 @@ export function hydrateInstance(
1125 // TODO: Possibly defer this until the commit phase where all the events
1126 // get attached.
1127 updateFiberProps(instance, props);
1096 - let parentNamespace: string;
1097 - if (__DEV__) {
1098 - const hostContextDev = ((hostContext: any): HostContextDev);
1099 - parentNamespace = hostContextDev.namespace;
1100 - } else {
1101 - parentNamespace = ((hostContext: any): HostContextProd);
1102 - }
1128
1129 // TODO: Temporary hack to check if we're in a concurrent root. We can delete
1130 // when the legacy root API is removed.
1131 const isConcurrentMode =
1132 ((internalInstanceHandle: Fiber).mode & ConcurrentMode) !== NoMode;
1133
1134 + let parentNamespace;
1135 + if (__DEV__) {
1136 + const hostContextDev = ((hostContext: any): HostContextDev);
1137 + parentNamespace = hostContextDev.namespace;
1138 + } else {
1139 + const hostContextProd = ((hostContext: any): HostContextProd);
1140 + parentNamespace = hostContextProd;
1141 + }
1142 +
1143 return diffHydratedProperties(
1144 instance,
1145 type,
1146 props,
1113 - parentNamespace,
1147 isConcurrentMode,
1148 shouldWarnDev,
1149 + parentNamespace,
1150 );
1151 }
1152
@@ -1584,7 +1618,7 @@ export function isHostHoistableType(
1618 hostContext: HostContext,
1619 ): boolean {
1620 let outsideHostContainerContext: boolean;
1587 - let namespace: string;
1621 + let namespace: HostContextProd;
1622 if (__DEV__) {
1623 const hostContextDev: HostContextDev = (hostContext: any);
1624 // We can only render resources when we are not within the host container context
@@ -1595,17 +1629,41 @@ export function isHostHoistableType(
1629 const hostContextProd: HostContextProd = (hostContext: any);
1630 namespace = hostContextProd;
1631 }
1632 +
1633 + // Global opt out of hoisting for anything in SVG Namespace or anything with an itemProp inside an itemScope
1634 + if (namespace === SVG_NAMESPACE || props.itemProp != null) {
1635 + if (__DEV__) {
1636 + if (
1637 + outsideHostContainerContext &&
1638 + props.itemProp != null &&
1639 + (type === 'meta' ||
1640 + type === 'title' ||
1641 + type === 'style' ||
1642 + type === 'link' ||
1643 + type === 'script')
1644 + ) {
1645 + console.error(
1646 + 'Cannot render a <%s> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an' +
1647 + ' `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <%s> remove the `itemProp` prop.' +
1648 + ' Otherwise, try moving this tag into the <head> or <body> of the Document.',
1649 + type,
1650 + type,
1651 + );
1652 + }
1653 + }
1654 + return false;
1655 + }
1656 +
1657 switch (type) {
1658 case 'meta':
1659 case 'title': {
1601 - return namespace !== SVG_NAMESPACE;
1660 + return true;
1661 }
1662 case 'style': {
1663 if (
1664 typeof props.precedence !== 'string' ||
1665 typeof props.href !== 'string' ||
1607 - props.href === '' ||
1608 - namespace === SVG_NAMESPACE
1666 + props.href === ''
1667 ) {
1668 if (__DEV__) {
1669 if (outsideHostContainerContext) {
@@ -1629,8 +1687,7 @@ export function isHostHoistableType(
1687 typeof props.href !== 'string' ||
1688 props.href === '' ||
1689 props.onLoad ||
1632 - props.onError ||
1633 - namespace === SVG_NAMESPACE
1690 + props.onError
1691 ) {
1692 if (__DEV__) {
1693 if (
@@ -1686,8 +1743,7 @@ export function isHostHoistableType(
1743 props.onLoad ||
1744 props.onError ||
1745 typeof props.src !== 'string' ||
1689 - !props.src ||
1690 - namespace === SVG_NAMESPACE
1746 + !props.src
1747 ) {
1748 if (__DEV__) {
1749 if (outsideHostContainerContext) {
@@ -1771,8 +1827,8 @@ export function resolveSingletonInstance(
1827 validateDOMNestingDev: boolean,
1828 ): Instance {
1829 if (__DEV__) {
1830 + const hostContextDev = ((hostContext: any): HostContextDev);
1831 if (validateDOMNestingDev) {
1775 - const hostContextDev = ((hostContext: any): HostContextDev);
1832 validateDOMNesting(type, null, hostContextDev.ancestorInfo);
1833 }
1834 }
packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
+13 -2
@@ -1257,7 +1257,11 @@ function pushMeta(
1257 noscriptTagInScope: boolean,
1258 ): null {
1259 if (enableFloat) {
1260 - if (insertionMode === SVG_MODE || noscriptTagInScope) {
1260 + if (
1261 + insertionMode === SVG_MODE ||
1262 + noscriptTagInScope ||
1263 + props.itemProp != null
1264 + ) {
1265 return pushSelfClosing(target, props, 'meta');
1266 } else {
1267 if (textEmbedded) {
@@ -1293,6 +1297,7 @@ function pushLink(
1297 if (
1298 insertionMode === SVG_MODE ||
1299 noscriptTagInScope ||
1300 + props.itemProp != null ||
1301 typeof rel !== 'string' ||
1302 typeof href !== 'string' ||
1303 href === ''
@@ -1573,6 +1578,7 @@ function pushStyle(
1578 if (
1579 insertionMode === SVG_MODE ||
1580 noscriptTagInScope ||
1581 + props.itemProp != null ||
1582 typeof precedence !== 'string' ||
1583 typeof href !== 'string' ||
1584 href === ''
@@ -1843,7 +1849,11 @@ function pushTitle(
1849 }
1850
1851 if (enableFloat) {
1846 - if (insertionMode !== SVG_MODE && !noscriptTagInScope) {
1852 + if (
1853 + insertionMode !== SVG_MODE &&
1854 + !noscriptTagInScope &&
1855 + props.itemProp == null
1856 + ) {
1857 pushTitleImpl(responseState.hoistableChunks, props);
1858 return null;
1859 } else {
@@ -2034,6 +2044,7 @@ function pushScript(
2044 if (
2045 insertionMode === SVG_MODE ||
2046 noscriptTagInScope ||
2047 + props.itemProp != null ||
2048 typeof props.src !== 'string' ||
2049 !props.src
2050 ) {
packages/react-dom-bindings/src/shared/DOMNamespaces.js
+1 -1
@@ -4,7 +4,7 @@
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 - * @noflow
7 + * @flow
8 */
9
10 export const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+336 -1
@@ -369,7 +369,6 @@ describe('ReactDOMFloat', () => {
369 <meta property="foo" content="bar" />
370 <title>foo</title>
371 <link rel="foo" href="bar" />
372 - <link rel="foo" href="bar" />
372 <noscript>&lt;link rel="icon" href="icon"/&gt;</noscript>
373 <base target="foo" href="bar" />
374 <script async="" src="foo" />
@@ -2304,6 +2303,342 @@ body {
2303 );
2304 });
2305
2306 + it('does not hoist anything with an itemprop prop', async () => {
2307 + function App() {
2308 + return (
2309 + <html>
2310 + <head>
2311 + <meta itemProp="outside" content="unscoped" />
2312 + <link itemProp="link" rel="foo" href="foo" />
2313 + <title itemProp="outside-title">title</title>
2314 + <link
2315 + itemProp="outside-stylesheet"
2316 + rel="stylesheet"
2317 + href="bar"
2318 + precedence="default"
2319 + />
2320 + <style itemProp="outside-style" href="baz" precedence="default">
2321 + outside style
2322 + </style>
2323 + <script itemProp="outside-script" async={true} src="qux" />
2324 + </head>
2325 + <body>
2326 + <div itemScope={true}>
2327 + <div>
2328 + <meta itemProp="inside-meta" content="scoped" />
2329 + <link itemProp="inside-link" rel="foo" href="foo" />
2330 + <title itemProp="inside-title">title</title>
2331 + <link
2332 + itemProp="inside-stylesheet"
2333 + rel="stylesheet"
2334 + href="bar"
2335 + precedence="default"
2336 + />
2337 + <style itemProp="inside-style" href="baz" precedence="default">
2338 + inside style
2339 + </style>
2340 + <script itemProp="inside-script" async={true} src="qux" />
2341 + </div>
2342 + </div>
2343 + </body>
2344 + </html>
2345 + );
2346 + }
2347 + await actIntoEmptyDocument(() => {
2348 + renderToPipeableStream(<App />).pipe(writable);
2349 + });
2350 +
2351 + expect(getMeaningfulChildren(document)).toEqual(
2352 + <html>
2353 + <head>
2354 + <meta itemprop="outside" content="unscoped" />
2355 + <link itemprop="link" rel="foo" href="foo" />
2356 + <title itemprop="outside-title">title</title>
2357 + <link
2358 + itemprop="outside-stylesheet"
2359 + rel="stylesheet"
2360 + href="bar"
2361 + precedence="default"
2362 + />
2363 + <style itemprop="outside-style" href="baz" precedence="default">
2364 + outside style
2365 + </style>
2366 + <script itemprop="outside-script" async="" src="qux" />
2367 + </head>
2368 + <body>
2369 + <div itemscope="">
2370 + <div>
2371 + <meta itemprop="inside-meta" content="scoped" />
2372 + <link itemprop="inside-link" rel="foo" href="foo" />
2373 + <title itemprop="inside-title">title</title>
2374 + <link
2375 + itemprop="inside-stylesheet"
2376 + rel="stylesheet"
2377 + href="bar"
2378 + precedence="default"
2379 + />
2380 + <style itemprop="inside-style" href="baz" precedence="default">
2381 + inside style
2382 + </style>
2383 + <script itemprop="inside-script" async="" src="qux" />
2384 + </div>
2385 + </div>
2386 + </body>
2387 + </html>,
2388 + );
2389 +
2390 + ReactDOMClient.hydrateRoot(document, <App />);
2391 + expect(Scheduler).toFlushWithoutYielding();
2392 +
2393 + expect(getMeaningfulChildren(document)).toEqual(
2394 + <html>
2395 + <head>
2396 + <meta itemprop="outside" content="unscoped" />
2397 + <link itemprop="link" rel="foo" href="foo" />
2398 + <title itemprop="outside-title">title</title>
2399 + <link
2400 + itemprop="outside-stylesheet"
2401 + rel="stylesheet"
2402 + href="bar"
2403 + precedence="default"
2404 + />
2405 + <style itemprop="outside-style" href="baz" precedence="default">
2406 + outside style
2407 + </style>
2408 + <script itemprop="outside-script" async="" src="qux" />
2409 + </head>
2410 + <body>
2411 + <div itemscope="">
2412 + <div>
2413 + <meta itemprop="inside-meta" content="scoped" />
2414 + <link itemprop="inside-link" rel="foo" href="foo" />
2415 + <title itemprop="inside-title">title</title>
2416 + <link
2417 + itemprop="inside-stylesheet"
2418 + rel="stylesheet"
2419 + href="bar"
2420 + precedence="default"
2421 + />
2422 + <style itemprop="inside-style" href="baz" precedence="default">
2423 + inside style
2424 + </style>
2425 + <script itemprop="inside-script" async="" src="qux" />
2426 + </div>
2427 + </div>
2428 + </body>
2429 + </html>,
2430 + );
2431 + });
2432 +
2433 + it('warns if you render a tag with itemProp outside <body> or <head>', async () => {
2434 + const root = ReactDOMClient.createRoot(document);
2435 + root.render(
2436 + <html>
2437 + <meta itemProp="foo" />
2438 + <title itemProp="foo">title</title>
2439 + <style itemProp="foo">style</style>
2440 + <link itemProp="foo" />
2441 + <script itemProp="foo" />
2442 + </html>,
2443 + );
2444 + expect(() => {
2445 + expect(Scheduler).toFlushWithoutYielding();
2446 + }).toErrorDev([
2447 + '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.',
2448 + '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.',
2449 + '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.',
2450 + '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.',
2451 + '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.',
2452 + 'validateDOMNesting(...): <meta> cannot appear as a child of <html>',
2453 + 'validateDOMNesting(...): <title> cannot appear as a child of <html>',
2454 + 'validateDOMNesting(...): <style> cannot appear as a child of <html>',
2455 + 'validateDOMNesting(...): <link> cannot appear as a child of <html>',
2456 + 'validateDOMNesting(...): <script> cannot appear as a child of <html>',
2457 + ]);
2458 + });
2459 +
2460 + // @gate enableFloat
2461 + it('can hydrate resources and components in the head and body even if a browser or 3rd party script injects extra html nodes', async () => {
2462 + // This is a stress test case for hydrating a complex combination of hoistable elements, hoistable resources and host components
2463 + // in an environment that has been manipulated by 3rd party scripts/extensions to modify the <head> and <body>
2464 + function App() {
2465 + return (
2466 + <>
2467 + <link rel="foo" href="foo" />
2468 + <script async={true} src="rendered" />
2469 + <link rel="stylesheet" href="stylesheet" precedence="default" />
2470 + <html itemScope={true}>
2471 + <head>
2472 + {/* Component */}
2473 + <link rel="stylesheet" href="stylesheet" />
2474 + <script src="sync rendered" data-meaningful="" />
2475 + <style>{'body { background-color: red; }'}</style>
2476 + <script src="async rendered" async={true} onLoad={() => {}} />
2477 + <noscript>
2478 + <meta name="noscript" content="noscript" />
2479 + </noscript>
2480 + <link rel="foo" href="foo" onLoad={() => {}} />
2481 + </head>
2482 + <body>
2483 + {/* Component because it has itemProp */}
2484 + <meta name="foo" content="foo" itemProp="a prop" />
2485 + {/* regular Hoistable */}
2486 + <meta name="foo" content="foo" />
2487 + {/* regular Hoistable */}
2488 + <title>title</title>
2489 + <div itemScope={true}>
2490 + <div>
2491 + <div>deep hello</div>
2492 + {/* Component because it has itemProp */}
2493 + <meta name="foo" content="foo" itemProp="a prop" />
2494 + </div>
2495 + </div>
2496 + </body>
2497 + </html>
2498 + <link rel="foo" href="foo" />
2499 + </>
2500 + );
2501 + }
2502 +
2503 + await actIntoEmptyDocument(() => {
2504 + renderToPipeableStream(<App />).pipe(writable);
2505 + });
2506 +
2507 + expect(getMeaningfulChildren(document)).toEqual(
2508 + <html itemscope="">
2509 + <head>
2510 + {/* Hoisted Resources and elements */}
2511 + <link rel="stylesheet" href="stylesheet" data-precedence="default" />
2512 + <script async="" src="rendered" />
2513 + <link rel="preload" as="script" href="sync rendered" />
2514 + <link rel="preload" as="script" href="async rendered" />
2515 + <link rel="foo" href="foo" />
2516 + <meta name="foo" content="foo" />
2517 + <title>title</title>
2518 + <link rel="foo" href="foo" />
2519 + {/* rendered host components */}
2520 + <link rel="stylesheet" href="stylesheet" />
2521 + <script src="sync rendered" data-meaningful="" />
2522 + <style>{'body { background-color: red; }'}</style>
2523 + <noscript>&lt;meta name="noscript" content="noscript"/&gt;</noscript>
2524 + <link rel="foo" href="foo" />
2525 + </head>
2526 + <body>
2527 + <meta name="foo" content="foo" itemprop="a prop" />
2528 + <div itemscope="">
2529 + <div>
2530 + <div>deep hello</div>
2531 + <meta name="foo" content="foo" itemprop="a prop" />
2532 + </div>
2533 + </div>
2534 + </body>
2535 + </html>,
2536 + );
2537 +
2538 + // We inject some styles, divs, scripts into the begginning, middle, and end
2539 + // of the head / body.
2540 + const injectedStyle = document.createElement('style');
2541 + injectedStyle.textContent = 'body { background-color: blue; }';
2542 + document.head.prepend(injectedStyle.cloneNode(true));
2543 + document.head.appendChild(injectedStyle.cloneNode(true));
2544 + document.body.prepend(injectedStyle.cloneNode(true));
2545 + document.body.appendChild(injectedStyle.cloneNode(true));
2546 +
2547 + const injectedDiv = document.createElement('div');
2548 + document.head.prepend(injectedDiv);
2549 + document.head.appendChild(injectedDiv.cloneNode(true));
2550 + // We do not prepend a <div> in body because this will conflict with hyration
2551 + // We still mostly hydrate by matchign tag and <div> does not have any attributes to
2552 + // differentiate between likely-inject and likely-rendered cases. If a <div> is prepended
2553 + // in the <body> and you render a <div> as the first child of <body> there will be a conflict.
2554 + // We consider this a rare edge case and even if it does happen the fallback to client rendering
2555 + // should patch up the DOM correctly
2556 + document.body.appendChild(injectedDiv.cloneNode(true));
2557 +
2558 + const injectedScript = document.createElement('script');
2559 + injectedScript.setAttribute('async', '');
2560 + injectedScript.setAttribute('src', 'injected');
2561 + document.head.prepend(injectedScript);
2562 + document.head.appendChild(injectedScript.cloneNode(true));
2563 + document.body.prepend(injectedScript.cloneNode(true));
2564 + document.body.appendChild(injectedScript.cloneNode(true));
2565 +
2566 + // We hydrate the same App and confirm the output is identical except for the async
2567 + // script insertion that happens because we do not SSR async scripts with load handlers.
2568 + // All the extra inject nodes are preset
2569 + const root = ReactDOMClient.hydrateRoot(document, <App />);
2570 + expect(Scheduler).toFlushWithoutYielding();
2571 + expect(getMeaningfulChildren(document)).toEqual(
2572 + <html itemscope="">
2573 + <head>
2574 + <script async="" src="injected" />
2575 + <div />
2576 + <style>{'body { background-color: blue; }'}</style>
2577 + <link rel="stylesheet" href="stylesheet" data-precedence="default" />
2578 + <script async="" src="rendered" />
2579 + <link rel="preload" as="script" href="sync rendered" />
2580 + <link rel="preload" as="script" href="async rendered" />
2581 + <link rel="foo" href="foo" />
2582 + <meta name="foo" content="foo" />
2583 + <title>title</title>
2584 + <link rel="foo" href="foo" />
2585 + <link rel="stylesheet" href="stylesheet" />
2586 + <script src="sync rendered" data-meaningful="" />
2587 + <style>{'body { background-color: red; }'}</style>
2588 + <script src="async rendered" async="" />
2589 + <noscript>&lt;meta name="noscript" content="noscript"/&gt;</noscript>
2590 + <link rel="foo" href="foo" />
2591 + <style>{'body { background-color: blue; }'}</style>
2592 + <div />
2593 + <script async="" src="injected" />
2594 + </head>
2595 + <body>
2596 + <script async="" src="injected" />
2597 + <style>{'body { background-color: blue; }'}</style>
2598 + <meta name="foo" content="foo" itemprop="a prop" />
2599 + <div itemscope="">
2600 + <div>
2601 + <div>deep hello</div>
2602 + <meta name="foo" content="foo" itemprop="a prop" />
2603 + </div>
2604 + </div>
2605 + <style>{'body { background-color: blue; }'}</style>
2606 + <div />
2607 + <script async="" src="injected" />
2608 + </body>
2609 + </html>,
2610 + );
2611 +
2612 + // We unmount. The nodes that remain are
2613 + // 1. Hoisted resources (we don't clean these up on unmount to address races with streaming suspense and navigation)
2614 + // 2. preloads that are injected to hint the browser to load a resource but are not associated to Fibers directly
2615 + // 3. Nodes that React skipped over during hydration
2616 + root.unmount();
2617 + expect(getMeaningfulChildren(document)).toEqual(
2618 + <html>
2619 + <head>
2620 + <script async="" src="injected" />
2621 + <div />
2622 + <style>{'body { background-color: blue; }'}</style>
2623 + <link rel="stylesheet" href="stylesheet" data-precedence="default" />
2624 + <script async="" src="rendered" />
2625 + <link rel="preload" as="script" href="sync rendered" />
2626 + <link rel="preload" as="script" href="async rendered" />
2627 + <style>{'body { background-color: blue; }'}</style>
2628 + <div />
2629 + <script async="" src="injected" />
2630 + </head>
2631 + <body>
2632 + <script async="" src="injected" />
2633 + <style>{'body { background-color: blue; }'}</style>
2634 + <style>{'body { background-color: blue; }'}</style>
2635 + <div />
2636 + <script async="" src="injected" />
2637 + </body>
2638 + </html>,
2639 + );
2640 + });
2641 +
2642 describe('ReactDOM.prefetchDNS(href)', () => {
2643 it('creates a dns-prefetch resource when called', async () => {
2644 function App({url}) {
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+6 -11
@@ -3380,7 +3380,6 @@ describe('ReactDOMServerPartialHydration', () => {
3380 itHydratesWithoutMismatch('an empty string in class component', TestAppClass);
3381
3382 it('fallback to client render on hydration mismatch at root', async () => {
3383 - let isClient = false;
3383 let suspend = true;
3384 let resolve;
3385 const promise = new Promise((res, rej) => {
@@ -3389,36 +3388,35 @@ describe('ReactDOMServerPartialHydration', () => {
3388 res();
3389 };
3390 });
3392 - function App() {
3391 + function App({isClient}) {
3392 return (
3393 <>
3394 <Suspense fallback={<div>Loading</div>}>
3396 - <ChildThatSuspends id={1} />
3395 + <ChildThatSuspends id={1} isClient={isClient} />
3396 </Suspense>
3397 {isClient ? <span>client</span> : <div>server</div>}
3398 <Suspense fallback={<div>Loading</div>}>
3400 - <ChildThatSuspends id={2} />
3399 + <ChildThatSuspends id={2} isClient={isClient} />
3400 </Suspense>
3401 </>
3402 );
3403 }
3405 - function ChildThatSuspends({id}) {
3404 + function ChildThatSuspends({id, isClient}) {
3405 if (isClient && suspend) {
3406 throw promise;
3407 }
3408 return <div>{id}</div>;
3409 }
3410
3412 - const finalHTML = ReactDOMServer.renderToString(<App />);
3411 + const finalHTML = ReactDOMServer.renderToString(<App isClient={false} />);
3412
3413 const container = document.createElement('div');
3414 document.body.appendChild(container);
3415 container.innerHTML = finalHTML;
3417 - isClient = true;
3416
3417 expect(() => {
3418 act(() => {
3421 - ReactDOMClient.hydrateRoot(container, <App />, {
3419 + ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
3420 onRecoverableError(error) {
3421 Scheduler.log('Log recoverable error: ' + error.message);
3422 },
@@ -3435,9 +3433,6 @@ describe('ReactDOMServerPartialHydration', () => {
3433 {withoutStack: 1},
3434 );
3435 assertLog([
3438 - 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
3439 - // TODO: There were multiple mismatches in a single container. Should
3440 - // we attempt to de-dupe them?
3436 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
3437 '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.',
3438 ]);
packages/react-reconciler/src/ReactFiberBeginWork.js
+4 -2
@@ -227,6 +227,8 @@ import {
227 resetHydrationState,
228 claimHydratableSingleton,
229 tryToClaimNextHydratableInstance,
230 + tryToClaimNextHydratableTextInstance,
231 + tryToClaimNextHydratableSuspenseInstance,
232 warnIfHydrating,
233 queueHydrationError,
234 } from './ReactFiberHydrationContext';
@@ -1694,7 +1696,7 @@ function updateHostSingleton(
1696
1697 function updateHostText(current: null | Fiber, workInProgress: Fiber) {
1698 if (current === null) {
1697 - tryToClaimNextHydratableInstance(workInProgress);
1699 + tryToClaimNextHydratableTextInstance(workInProgress);
1700 }
1701 // Nothing to do here. This is terminal. We'll do the completion step
1702 // immediately after.
@@ -2250,7 +2252,7 @@ function updateSuspenseComponent(
2252 } else {
2253 pushFallbackTreeSuspenseHandler(workInProgress);
2254 }
2253 - tryToClaimNextHydratableInstance(workInProgress);
2255 + tryToClaimNextHydratableSuspenseInstance(workInProgress);
2256 // This could've been a dehydrated suspense component.
2257 const suspenseState: null | SuspenseState = workInProgress.memoizedState;
2258 if (suspenseState !== null) {
packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
+8 -4
@@ -21,10 +21,8 @@ function shim(...args: any): empty {
21 // Hydration (when unsupported)
22 export type SuspenseInstance = mixed;
23 export const supportsHydration = false;
24 -export const isHydratable = shim;
25 -export const canHydrateInstance = shim;
26 -export const canHydrateTextInstance = shim;
27 -export const canHydrateSuspenseInstance = shim;
24 +export const isHydratableType = shim;
25 +export const isHydratableText = shim;
26 export const isSuspenseInstancePending = shim;
27 export const isSuspenseInstanceFallback = shim;
28 export const getSuspenseInstanceFallbackErrorDetails = shim;
@@ -33,6 +31,12 @@ export const getNextHydratableSibling = shim;
31 export const getFirstHydratableChild = shim;
32 export const getFirstHydratableChildWithinContainer = shim;
33 export const getFirstHydratableChildWithinSuspenseInstance = shim;
34 +export const shouldSkipHydratableForInstance = shim;
35 +export const shouldSkipHydratableForTextInstance = shim;
36 +export const shouldSkipHydratableForSuspenseInstance = shim;
37 +export const canHydrateInstance = shim;
38 +export const canHydrateTextInstance = shim;
39 +export const canHydrateSuspenseInstance = shim;
40 export const hydrateInstance = shim;
41 export const hydrateTextInstance = shim;
42 export const hydrateSuspenseInstance = shim;
packages/react-reconciler/src/ReactFiberHostContext.js
+1 -1
@@ -40,7 +40,7 @@ function getRootHostContainer(): Container {
40 return rootInstance;
41 }
42
43 -function pushHostContainer(fiber: Fiber, nextRootInstance: Container) {
43 +function pushHostContainer(fiber: Fiber, nextRootInstance: Container): void {
44 // Push current root instance onto the stack;
45 // This allows us to reset root when portals are popped.
46 push(rootInstanceStackCursor, nextRootInstance, fiber);
packages/react-reconciler/src/ReactFiberHydrationContext.js
+268 -79
@@ -45,10 +45,6 @@ import {
45 shouldSetTextContent,
46 supportsHydration,
47 supportsSingletons,
48 - isHydratable,
49 - canHydrateInstance,
50 - canHydrateTextInstance,
51 - canHydrateSuspenseInstance,
48 getNextHydratableSibling,
49 getFirstHydratableChild,
50 getFirstHydratableChildWithinContainer,
@@ -73,6 +69,14 @@ import {
69 didNotFindHydratableTextInstance,
70 didNotFindHydratableSuspenseInstance,
71 resolveSingletonInstance,
72 + shouldSkipHydratableForInstance,
73 + shouldSkipHydratableForTextInstance,
74 + shouldSkipHydratableForSuspenseInstance,
75 + canHydrateInstance,
76 + canHydrateTextInstance,
77 + canHydrateSuspenseInstance,
78 + isHydratableType,
79 + isHydratableText,
80 } from './ReactFiberHostConfig';
81 import {OffscreenLane} from './ReactFiberLane';
82 import {
@@ -95,6 +99,8 @@ let didSuspendOrErrorDEV: boolean = false;
99 // Hydration errors that were thrown inside this boundary
100 let hydrationErrors: Array<CapturedValue<mixed>> | null = null;
101
102 +let rootOrSingletonContext = false;
103 +
104 function warnIfHydrating() {
105 if (__DEV__) {
106 if (isHydrating) {
@@ -130,6 +136,7 @@ function enterHydrationState(fiber: Fiber): boolean {
136 isHydrating = true;
137 hydrationErrors = null;
138 didSuspendOrErrorDEV = false;
139 + rootOrSingletonContext = true;
140 return true;
141 }
142
@@ -147,6 +154,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
154 isHydrating = true;
155 hydrationErrors = null;
156 didSuspendOrErrorDEV = false;
157 + rootOrSingletonContext = false;
158 if (treeContext !== null) {
159 restoreSuspendedTreeContext(fiber, treeContext);
160 }
@@ -336,63 +344,62 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
344 warnNonhydratedInstance(returnFiber, fiber);
345 }
346
339 -function tryHydrate(fiber: Fiber, nextInstance: any) {
340 - switch (fiber.tag) {
341 - // HostSingleton is intentionally omitted. the hydration pathway for singletons is non-fallible
342 - // you can find it inlined in claimHydratableSingleton
343 - case HostComponent: {
344 - const type = fiber.type;
345 - const props = fiber.pendingProps;
346 - const instance = canHydrateInstance(nextInstance, type, props);
347 - if (instance !== null) {
348 - fiber.stateNode = (instance: Instance);
349 - hydrationParentFiber = fiber;
350 - nextHydratableInstance = getFirstHydratableChild(instance);
351 - return true;
352 - }
353 - return false;
354 - }
355 - case HostText: {
356 - const text = fiber.pendingProps;
357 - const textInstance = canHydrateTextInstance(nextInstance, text);
358 - if (textInstance !== null) {
359 - fiber.stateNode = (textInstance: TextInstance);
360 - hydrationParentFiber = fiber;
361 - // Text Instances don't have children so there's nothing to hydrate.
362 - nextHydratableInstance = null;
363 - return true;
364 - }
365 - return false;
366 - }
367 - case SuspenseComponent: {
368 - const suspenseInstance: null | SuspenseInstance =
369 - canHydrateSuspenseInstance(nextInstance);
370 - if (suspenseInstance !== null) {
371 - const suspenseState: SuspenseState = {
372 - dehydrated: suspenseInstance,
373 - treeContext: getSuspendedTreeContext(),
374 - retryLane: OffscreenLane,
375 - };
376 - fiber.memoizedState = suspenseState;
377 - // Store the dehydrated fragment as a child fiber.
378 - // This simplifies the code for getHostSibling and deleting nodes,
379 - // since it doesn't have to consider all Suspense boundaries and
380 - // check if they're dehydrated ones or not.
381 - const dehydratedFragment =
382 - createFiberFromDehydratedFragment(suspenseInstance);
383 - dehydratedFragment.return = fiber;
384 - fiber.child = dehydratedFragment;
385 - hydrationParentFiber = fiber;
386 - // While a Suspense Instance does have children, we won't step into
387 - // it during the first pass. Instead, we'll reenter it later.
388 - nextHydratableInstance = null;
389 - return true;
390 - }
391 - return false;
392 - }
393 - default:
394 - return false;
347 +function tryHydrateInstance(fiber: Fiber, nextInstance: any) {
348 + // fiber is a HostComponent Fiber
349 + const instance = canHydrateInstance(
350 + nextInstance,
351 + fiber.type,
352 + fiber.pendingProps,
353 + );
354 + if (instance !== null) {
355 + fiber.stateNode = (instance: Instance);
356 + hydrationParentFiber = fiber;
357 + nextHydratableInstance = getFirstHydratableChild(instance);
358 + rootOrSingletonContext = false;
359 + return true;
360 }
361 + return false;
362 +}
363 +
364 +function tryHydrateText(fiber: Fiber, nextInstance: any) {
365 + // fiber is a HostText Fiber
366 + const text = fiber.pendingProps;
367 + const textInstance = canHydrateTextInstance(nextInstance, text);
368 + if (textInstance !== null) {
369 + fiber.stateNode = (textInstance: TextInstance);
370 + hydrationParentFiber = fiber;
371 + // Text Instances don't have children so there's nothing to hydrate.
372 + nextHydratableInstance = null;
373 + return true;
374 + }
375 + return false;
376 +}
377 +
378 +function tryHydrateSuspense(fiber: Fiber, nextInstance: any) {
379 + // fiber is a SuspenseComponent Fiber
380 + const suspenseInstance = canHydrateSuspenseInstance(nextInstance);
381 + if (suspenseInstance !== null) {
382 + const suspenseState: SuspenseState = {
383 + dehydrated: suspenseInstance,
384 + treeContext: getSuspendedTreeContext(),
385 + retryLane: OffscreenLane,
386 + };
387 + fiber.memoizedState = suspenseState;
388 + // Store the dehydrated fragment as a child fiber.
389 + // This simplifies the code for getHostSibling and deleting nodes,
390 + // since it doesn't have to consider all Suspense boundaries and
391 + // check if they're dehydrated ones or not.
392 + const dehydratedFragment =
393 + createFiberFromDehydratedFragment(suspenseInstance);
394 + dehydratedFragment.return = fiber;
395 + fiber.child = dehydratedFragment;
396 + hydrationParentFiber = fiber;
397 + // While a Suspense Instance does have children, we won't step into
398 + // it during the first pass. Instead, we'll reenter it later.
399 + nextHydratableInstance = null;
400 + return true;
401 + }
402 + return false;
403 }
404
405 function shouldClientRenderOnMismatch(fiber: Fiber) {
@@ -424,22 +431,189 @@ function claimHydratableSingleton(fiber: Fiber): void {
431 false,
432 ));
433 hydrationParentFiber = fiber;
434 + rootOrSingletonContext = true;
435 nextHydratableInstance = getFirstHydratableChild(instance);
436 }
437 }
438
439 +function advanceToFirstAttempableInstance(fiber: Fiber) {
440 + // fiber is HostComponent Fiber
441 + while (
442 + nextHydratableInstance &&
443 + shouldSkipHydratableForInstance(
444 + nextHydratableInstance,
445 + fiber.type,
446 + fiber.pendingProps,
447 + )
448 + ) {
449 + // Flow doesn't understand that inside this block nextHydratableInstance is not null
450 + const instance: HydratableInstance = (nextHydratableInstance: any);
451 + nextHydratableInstance = getNextHydratableSibling(instance);
452 + }
453 +}
454 +
455 +function advanceToFirstAttempableTextInstance() {
456 + while (
457 + nextHydratableInstance &&
458 + shouldSkipHydratableForTextInstance(nextHydratableInstance)
459 + ) {
460 + // Flow doesn't understand that inside this block nextHydratableInstance is not null
461 + const instance: HydratableInstance = (nextHydratableInstance: any);
462 + nextHydratableInstance = getNextHydratableSibling(instance);
463 + }
464 +}
465 +
466 +function advanceToFirstAttempableSuspenseInstance() {
467 + while (
468 + nextHydratableInstance &&
469 + shouldSkipHydratableForSuspenseInstance(nextHydratableInstance)
470 + ) {
471 + // Flow doesn't understand that inside this block nextHydratableInstance is not null
472 + const instance: HydratableInstance = (nextHydratableInstance: any);
473 + nextHydratableInstance = getNextHydratableSibling(instance);
474 + }
475 +}
476 +
477 function tryToClaimNextHydratableInstance(fiber: Fiber): void {
478 if (!isHydrating) {
479 return;
480 }
435 - if (enableFloat && !isHydratable(fiber.type, fiber.pendingProps)) {
436 - // This fiber never hydrates from the DOM and always does an insert
437 - fiber.flags = (fiber.flags & ~Hydrating) | Placement;
481 + if (enableFloat) {
482 + if (!isHydratableType(fiber.type, fiber.pendingProps)) {
483 + // This fiber never hydrates from the DOM and always does an insert
484 + fiber.flags = (fiber.flags & ~Hydrating) | Placement;
485 + isHydrating = false;
486 + hydrationParentFiber = fiber;
487 + return;
488 + }
489 + }
490 + const initialInstance = nextHydratableInstance;
491 + if (rootOrSingletonContext) {
492 + // We may need to skip past certain nodes in these contexts
493 + advanceToFirstAttempableInstance(fiber);
494 + }
495 + const nextInstance = nextHydratableInstance;
496 + if (!nextInstance) {
497 + if (shouldClientRenderOnMismatch(fiber)) {
498 + warnNonhydratedInstance((hydrationParentFiber: any), fiber);
499 + throwOnHydrationMismatch(fiber);
500 + }
501 + // Nothing to hydrate. Make it an insertion.
502 + insertNonHydratedInstance((hydrationParentFiber: any), fiber);
503 isHydrating = false;
504 hydrationParentFiber = fiber;
505 + nextHydratableInstance = initialInstance;
506 return;
507 }
442 - let nextInstance = nextHydratableInstance;
508 + const firstAttemptedInstance = nextInstance;
509 + if (!tryHydrateInstance(fiber, nextInstance)) {
510 + if (shouldClientRenderOnMismatch(fiber)) {
511 + warnNonhydratedInstance((hydrationParentFiber: any), fiber);
512 + throwOnHydrationMismatch(fiber);
513 + }
514 + // If we can't hydrate this instance let's try the next one.
515 + // We use this as a heuristic. It's based on intuition and not data so it
516 + // might be flawed or unnecessary.
517 + nextHydratableInstance = getNextHydratableSibling(nextInstance);
518 + const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
519 + if (rootOrSingletonContext) {
520 + // We may need to skip past certain nodes in these contexts
521 + advanceToFirstAttempableInstance(fiber);
522 + }
523 + if (
524 + !nextHydratableInstance ||
525 + !tryHydrateInstance(fiber, nextHydratableInstance)
526 + ) {
527 + // Nothing to hydrate. Make it an insertion.
528 + insertNonHydratedInstance((hydrationParentFiber: any), fiber);
529 + isHydrating = false;
530 + hydrationParentFiber = fiber;
531 + nextHydratableInstance = initialInstance;
532 + return;
533 + }
534 + // We matched the next one, we'll now assume that the first one was
535 + // superfluous and we'll delete it. Since we can't eagerly delete it
536 + // we'll have to schedule a deletion. To do that, this node needs a dummy
537 + // fiber associated with it.
538 + deleteHydratableInstance(prevHydrationParentFiber, firstAttemptedInstance);
539 + }
540 +}
541 +
542 +function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
543 + if (!isHydrating) {
544 + return;
545 + }
546 + const text = fiber.pendingProps;
547 + const isHydratable = isHydratableText(text);
548 +
549 + const initialInstance = nextHydratableInstance;
550 + if (rootOrSingletonContext && isHydratable) {
551 + // We may need to skip past certain nodes in these contexts.
552 + // We don't skip if the text is not hydratable because we know no hydratables
553 + // exist which could match this Fiber
554 + advanceToFirstAttempableTextInstance();
555 + }
556 + const nextInstance = nextHydratableInstance;
557 + if (!nextInstance || !isHydratable) {
558 + // We exclude non hydrabable text because we know there are no matching hydratables.
559 + // We either throw or insert depending on the render mode.
560 + if (shouldClientRenderOnMismatch(fiber)) {
561 + warnNonhydratedInstance((hydrationParentFiber: any), fiber);
562 + throwOnHydrationMismatch(fiber);
563 + }
564 + // Nothing to hydrate. Make it an insertion.
565 + insertNonHydratedInstance((hydrationParentFiber: any), fiber);
566 + isHydrating = false;
567 + hydrationParentFiber = fiber;
568 + nextHydratableInstance = initialInstance;
569 + return;
570 + }
571 + const firstAttemptedInstance = nextInstance;
572 + if (!tryHydrateText(fiber, nextInstance)) {
573 + if (shouldClientRenderOnMismatch(fiber)) {
574 + warnNonhydratedInstance((hydrationParentFiber: any), fiber);
575 + throwOnHydrationMismatch(fiber);
576 + }
577 + // If we can't hydrate this instance let's try the next one.
578 + // We use this as a heuristic. It's based on intuition and not data so it
579 + // might be flawed or unnecessary.
580 + nextHydratableInstance = getNextHydratableSibling(nextInstance);
581 + const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
582 +
583 + if (rootOrSingletonContext && isHydratable) {
584 + // We may need to skip past certain nodes in these contexts
585 + advanceToFirstAttempableTextInstance();
586 + }
587 +
588 + if (
589 + !nextHydratableInstance ||
590 + !tryHydrateText(fiber, nextHydratableInstance)
591 + ) {
592 + // Nothing to hydrate. Make it an insertion.
593 + insertNonHydratedInstance((hydrationParentFiber: any), fiber);
594 + isHydrating = false;
595 + hydrationParentFiber = fiber;
596 + nextHydratableInstance = initialInstance;
597 + return;
598 + }
599 + // We matched the next one, we'll now assume that the first one was
600 + // superfluous and we'll delete it. Since we can't eagerly delete it
601 + // we'll have to schedule a deletion. To do that, this node needs a dummy
602 + // fiber associated with it.
603 + deleteHydratableInstance(prevHydrationParentFiber, firstAttemptedInstance);
604 + }
605 +}
606 +
607 +function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
608 + if (!isHydrating) {
609 + return;
610 + }
611 + const initialInstance = nextHydratableInstance;
612 + if (rootOrSingletonContext) {
613 + // We may need to skip past certain nodes in these contexts
614 + advanceToFirstAttempableSuspenseInstance();
615 + }
616 + const nextInstance = nextHydratableInstance;
617 if (!nextInstance) {
618 if (shouldClientRenderOnMismatch(fiber)) {
619 warnNonhydratedInstance((hydrationParentFiber: any), fiber);
@@ -449,10 +623,11 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
623 insertNonHydratedInstance((hydrationParentFiber: any), fiber);
624 isHydrating = false;
625 hydrationParentFiber = fiber;
626 + nextHydratableInstance = initialInstance;
627 return;
628 }
629 const firstAttemptedInstance = nextInstance;
455 - if (!tryHydrate(fiber, nextInstance)) {
630 + if (!tryHydrateSuspense(fiber, nextInstance)) {
631 if (shouldClientRenderOnMismatch(fiber)) {
632 warnNonhydratedInstance((hydrationParentFiber: any), fiber);
633 throwOnHydrationMismatch(fiber);
@@ -460,13 +635,23 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
635 // If we can't hydrate this instance let's try the next one.
636 // We use this as a heuristic. It's based on intuition and not data so it
637 // might be flawed or unnecessary.
463 - nextInstance = getNextHydratableSibling(firstAttemptedInstance);
638 + nextHydratableInstance = getNextHydratableSibling(nextInstance);
639 const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
465 - if (!nextInstance || !tryHydrate(fiber, nextInstance)) {
640 +
641 + if (rootOrSingletonContext) {
642 + // We may need to skip past certain nodes in these contexts
643 + advanceToFirstAttempableSuspenseInstance();
644 + }
645 +
646 + if (
647 + !nextHydratableInstance ||
648 + !tryHydrateSuspense(fiber, nextHydratableInstance)
649 + ) {
650 // Nothing to hydrate. Make it an insertion.
651 insertNonHydratedInstance((hydrationParentFiber: any), fiber);
652 isHydrating = false;
653 hydrationParentFiber = fiber;
654 + nextHydratableInstance = initialInstance;
655 return;
656 }
657 // We matched the next one, we'll now assume that the first one was
@@ -616,19 +801,21 @@ function skipPastDehydratedSuspenseInstance(
801 }
802
803 function popToNextHostParent(fiber: Fiber): void {
619 - let parent = fiber.return;
620 - while (
621 - parent !== null &&
622 - parent.tag !== HostComponent &&
623 - parent.tag !== HostRoot &&
624 - parent.tag !== SuspenseComponent &&
625 - (!(enableHostSingletons && supportsSingletons)
626 - ? true
627 - : parent.tag !== HostSingleton)
628 - ) {
629 - parent = parent.return;
804 + hydrationParentFiber = fiber.return;
805 + while (hydrationParentFiber) {
806 + switch (hydrationParentFiber.tag) {
807 + case HostRoot:
808 + case HostSingleton:
809 + rootOrSingletonContext = true;
810 + return;
811 + case HostComponent:
812 + case SuspenseComponent:
813 + rootOrSingletonContext = false;
814 + return;
815 + default:
816 + hydrationParentFiber = hydrationParentFiber.return;
817 + }
818 }
631 - hydrationParentFiber = parent;
819 }
820
821 function popHydrationState(fiber: Fiber): boolean {
@@ -755,6 +942,8 @@ export {
942 resetHydrationState,
943 claimHydratableSingleton,
944 tryToClaimNextHydratableInstance,
945 + tryToClaimNextHydratableTextInstance,
946 + tryToClaimNextHydratableSuspenseInstance,
947 prepareToHydrateHostInstance,
948 prepareToHydrateHostTextInstance,
949 prepareToHydrateHostSuspenseInstance,
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+12 -5
@@ -130,11 +130,8 @@ export const cloneHiddenTextInstance = $$$hostConfig.cloneHiddenTextInstance;
130 // Hydration
131 // (optional)
132 // -------------------
133 -export const isHydratable = $$$hostConfig.isHydratable;
134 -export const canHydrateInstance = $$$hostConfig.canHydrateInstance;
135 -export const canHydrateTextInstance = $$$hostConfig.canHydrateTextInstance;
136 -export const canHydrateSuspenseInstance =
137 - $$$hostConfig.canHydrateSuspenseInstance;
133 +export const isHydratableType = $$$hostConfig.isHydratableType;
134 +export const isHydratableText = $$$hostConfig.isHydratableText;
135 export const isSuspenseInstancePending =
136 $$$hostConfig.isSuspenseInstancePending;
137 export const isSuspenseInstanceFallback =
@@ -149,6 +146,16 @@ export const getFirstHydratableChildWithinContainer =
146 $$$hostConfig.getFirstHydratableChildWithinContainer;
147 export const getFirstHydratableChildWithinSuspenseInstance =
148 $$$hostConfig.getFirstHydratableChildWithinSuspenseInstance;
149 +export const shouldSkipHydratableForInstance =
150 + $$$hostConfig.shouldSkipHydratableForInstance;
151 +export const shouldSkipHydratableForTextInstance =
152 + $$$hostConfig.shouldSkipHydratableForTextInstance;
153 +export const shouldSkipHydratableForSuspenseInstance =
154 + $$$hostConfig.shouldSkipHydratableForSuspenseInstance;
155 +export const canHydrateInstance = $$$hostConfig.canHydrateInstance;
156 +export const canHydrateTextInstance = $$$hostConfig.canHydrateTextInstance;
157 +export const canHydrateSuspenseInstance =
158 + $$$hostConfig.canHydrateSuspenseInstance;
159 export const hydrateInstance = $$$hostConfig.hydrateInstance;
160 export const hydrateTextInstance = $$$hostConfig.hydrateTextInstance;
161 export const hydrateSuspenseInstance = $$$hostConfig.hydrateSuspenseInstance;