mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[Float] support title tags as Resources (#25508)
Adds a category of Resources of type `head` which will be used to track the tags that go into the <head> Currently only implements for `<title>`. titles are keyed off their textContent so each time the title changes a new resource will be created. Currently insertion is done by prepending in the <head>. The argument here is that the newest title should "win" if there are multiple rendered. This also helps when a navigation or update causes a server rendered title to hang around but it is not the most recent one.
This commit is contained in:
@@ -7,11 +7,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {
|
||||
FloatRoot,
|
||||
StyleResource,
|
||||
ScriptResource,
|
||||
} from './ReactDOMFloatClient';
|
||||
import type {FloatRoot, RootResources} from './ReactDOMFloatClient';
|
||||
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
||||
import type {ReactScopeInstance} from 'shared/ReactTypes';
|
||||
import type {
|
||||
@@ -53,6 +49,7 @@ const internalEventHandlersKey = '__reactEvents$' + randomKey;
|
||||
const internalEventHandlerListenersKey = '__reactListeners$' + randomKey;
|
||||
const internalEventHandlesSetKey = '__reactHandles$' + randomKey;
|
||||
const internalRootNodeResourcesKey = '__reactResources$' + randomKey;
|
||||
const internalResourceMarker = '__reactMarker$' + randomKey;
|
||||
|
||||
export function detachDeletedInstance(node: Instance): void {
|
||||
// TODO: This function is only called on host components. I don't think all of
|
||||
@@ -282,15 +279,22 @@ export function doesTargetHaveEventHandle(
|
||||
return eventHandles.has(eventHandle);
|
||||
}
|
||||
|
||||
export function getResourcesFromRoot(
|
||||
root: FloatRoot,
|
||||
): {styles: Map<string, StyleResource>, scripts: Map<string, ScriptResource>} {
|
||||
export function getResourcesFromRoot(root: FloatRoot): RootResources {
|
||||
let resources = (root: any)[internalRootNodeResourcesKey];
|
||||
if (!resources) {
|
||||
resources = (root: any)[internalRootNodeResourcesKey] = {
|
||||
styles: new Map(),
|
||||
scripts: new Map(),
|
||||
head: new Map(),
|
||||
};
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
export function isMarkedResource(node: Node): boolean {
|
||||
return !!(node: any)[internalResourceMarker];
|
||||
}
|
||||
|
||||
export function markNodeAsResource(node: Node) {
|
||||
(node: any)[internalResourceMarker] = true;
|
||||
}
|
||||
|
||||
+190
-18
@@ -24,7 +24,10 @@ import {
|
||||
validatePreinitArguments,
|
||||
} from '../shared/ReactDOMResourceValidation';
|
||||
import {createElement, setInitialProperties} from './ReactDOMComponent';
|
||||
import {getResourcesFromRoot} from './ReactDOMComponentTree';
|
||||
import {
|
||||
getResourcesFromRoot,
|
||||
markNodeAsResource,
|
||||
} from './ReactDOMComponentTree';
|
||||
import {HTML_NAMESPACE} from '../shared/DOMNamespaces';
|
||||
import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
|
||||
|
||||
@@ -85,9 +88,28 @@ export type ScriptResource = {
|
||||
root: FloatRoot,
|
||||
};
|
||||
|
||||
type HeadProps = {
|
||||
[string]: mixed,
|
||||
};
|
||||
export type HeadResource = {
|
||||
type: 'head',
|
||||
instanceType: string,
|
||||
props: HeadProps,
|
||||
|
||||
count: number,
|
||||
instance: ?Element,
|
||||
root: Document,
|
||||
};
|
||||
|
||||
type Props = {[string]: mixed};
|
||||
|
||||
type Resource = StyleResource | ScriptResource | PreloadResource;
|
||||
type Resource = StyleResource | ScriptResource | PreloadResource | HeadResource;
|
||||
|
||||
export type RootResources = {
|
||||
styles: Map<string, StyleResource>,
|
||||
scripts: Map<string, ScriptResource>,
|
||||
head: Map<string, HeadResource>,
|
||||
};
|
||||
|
||||
// Brief on purpose due to insertion by script when streaming late boundaries
|
||||
// s = Status
|
||||
@@ -370,6 +392,10 @@ type ScriptQualifyingProps = {
|
||||
[string]: mixed,
|
||||
};
|
||||
|
||||
function getTitleKey(child: string | number): string {
|
||||
return 'title:' + child;
|
||||
}
|
||||
|
||||
// This function is called in begin work and we should always have a currentDocument set
|
||||
export function getResource(
|
||||
type: string,
|
||||
@@ -383,6 +409,30 @@ export function getResource(
|
||||
);
|
||||
}
|
||||
switch (type) {
|
||||
case 'title': {
|
||||
let child = pendingProps.children;
|
||||
if (Array.isArray(child) && child.length === 1) {
|
||||
child = child[0];
|
||||
}
|
||||
if (typeof child === 'string' || typeof child === 'number') {
|
||||
const headRoot: Document = getDocumentFromRoot(resourceRoot);
|
||||
const headResources = getResourcesFromRoot(headRoot).head;
|
||||
const key = getTitleKey(child);
|
||||
let resource = headResources.get(key);
|
||||
if (!resource) {
|
||||
const titleProps = titlePropsFromRawProps(child, pendingProps);
|
||||
resource = createHeadResource(
|
||||
headResources,
|
||||
headRoot,
|
||||
'title',
|
||||
key,
|
||||
titleProps,
|
||||
);
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'link': {
|
||||
const {rel} = pendingProps;
|
||||
switch (rel) {
|
||||
@@ -535,6 +585,15 @@ function preloadPropsFromRawProps(
|
||||
return Object.assign({}, rawBorrowedProps);
|
||||
}
|
||||
|
||||
function titlePropsFromRawProps(
|
||||
child: string | number,
|
||||
rawProps: Props,
|
||||
): HeadProps {
|
||||
const props: HeadProps = Object.assign({}, rawProps);
|
||||
props.children = child;
|
||||
return props;
|
||||
}
|
||||
|
||||
function stylePropsFromRawProps(rawProps: StyleQualifyingProps): StyleProps {
|
||||
const props: StyleProps = Object.assign({}, rawProps);
|
||||
props['data-precedence'] = rawProps.precedence;
|
||||
@@ -554,6 +613,9 @@ function scriptPropsFromRawProps(rawProps: ScriptQualifyingProps): ScriptProps {
|
||||
|
||||
export function acquireResource(resource: Resource): Instance {
|
||||
switch (resource.type) {
|
||||
case 'head': {
|
||||
return acquireHeadResource(resource);
|
||||
}
|
||||
case 'style': {
|
||||
return acquireStyleResource(resource);
|
||||
}
|
||||
@@ -571,14 +633,30 @@ export function acquireResource(resource: Resource): Instance {
|
||||
}
|
||||
}
|
||||
|
||||
export function releaseResource(resource: Resource) {
|
||||
export function releaseResource(resource: Resource): void {
|
||||
switch (resource.type) {
|
||||
case 'head': {
|
||||
return releaseHeadResource(resource);
|
||||
}
|
||||
case 'style': {
|
||||
resource.count--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function releaseHeadResource(resource: HeadResource): void {
|
||||
if (--resource.count === 0) {
|
||||
// the instance will have existed since we acquired it
|
||||
const instance: Instance = (resource.instance: any);
|
||||
const parent = instance.parentNode;
|
||||
if (parent) {
|
||||
parent.removeChild(instance);
|
||||
}
|
||||
resource.instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
function createResourceInstance(
|
||||
type: string,
|
||||
props: Object,
|
||||
@@ -586,9 +664,39 @@ function createResourceInstance(
|
||||
): Instance {
|
||||
const element = createElement(type, props, ownerDocument, HTML_NAMESPACE);
|
||||
setInitialProperties(element, type, props);
|
||||
markNodeAsResource(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
function createHeadResource(
|
||||
headResources: Map<string, HeadResource>,
|
||||
root: Document,
|
||||
instanceType: string,
|
||||
key: string,
|
||||
props: HeadProps,
|
||||
): HeadResource {
|
||||
if (__DEV__) {
|
||||
if (headResources.has(key)) {
|
||||
console.error(
|
||||
'createHeadResource was called when a head Resource matching the same key already exists. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const resource: HeadResource = {
|
||||
type: 'head',
|
||||
instanceType,
|
||||
props,
|
||||
|
||||
count: 0,
|
||||
instance: null,
|
||||
root,
|
||||
};
|
||||
|
||||
headResources.set(key, resource);
|
||||
return resource;
|
||||
}
|
||||
|
||||
function createStyleResource(
|
||||
styleResources: Map<string, StyleResource>,
|
||||
root: FloatRoot,
|
||||
@@ -754,6 +862,8 @@ function createScriptResource(
|
||||
(resource: any)._dev_preload_props = preloadProps;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
markNodeAsResource(existingEl);
|
||||
}
|
||||
|
||||
return resource;
|
||||
@@ -784,7 +894,9 @@ function createPreloadResource(
|
||||
);
|
||||
if (!element) {
|
||||
element = createResourceInstance('link', props, ownerDocument);
|
||||
insertResourceInstance(element, ownerDocument);
|
||||
appendResourceInstance(element, ownerDocument);
|
||||
} else {
|
||||
markNodeAsResource(element);
|
||||
}
|
||||
return {
|
||||
type: 'preload',
|
||||
@@ -795,8 +907,41 @@ function createPreloadResource(
|
||||
};
|
||||
}
|
||||
|
||||
function acquireHeadResource(resource: HeadResource): Instance {
|
||||
resource.count++;
|
||||
let instance = resource.instance;
|
||||
if (!instance) {
|
||||
const {props, root, instanceType} = resource;
|
||||
switch (instanceType) {
|
||||
case 'title': {
|
||||
const titles = root.querySelectorAll('title');
|
||||
for (let i = 0; i < titles.length; i++) {
|
||||
if (titles[i].textContent === props.children) {
|
||||
instance = resource.instance = titles[i];
|
||||
markNodeAsResource(instance);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
instance = resource.instance = createResourceInstance(
|
||||
instanceType,
|
||||
props,
|
||||
root,
|
||||
);
|
||||
|
||||
if (instanceType === 'title') {
|
||||
prependResourceInstance(instance, root);
|
||||
} else {
|
||||
appendResourceInstance(instance, root);
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
function acquireStyleResource(resource: StyleResource): Instance {
|
||||
if (!resource.instance) {
|
||||
let instance = resource.instance;
|
||||
if (!instance) {
|
||||
const {props, root, precedence} = resource;
|
||||
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
|
||||
props.href,
|
||||
@@ -805,7 +950,8 @@ function acquireStyleResource(resource: StyleResource): Instance {
|
||||
`link[rel="stylesheet"][data-precedence][href="${limitedEscapedHref}"]`,
|
||||
);
|
||||
if (existingEl) {
|
||||
resource.instance = existingEl;
|
||||
instance = resource.instance = existingEl;
|
||||
markNodeAsResource(instance);
|
||||
resource.preloaded = true;
|
||||
const loadingState: ?StyleResourceLoadingState = (existingEl: any)._p;
|
||||
if (loadingState) {
|
||||
@@ -830,7 +976,7 @@ function acquireStyleResource(resource: StyleResource): Instance {
|
||||
resource.loaded = true;
|
||||
}
|
||||
} else {
|
||||
const instance = createResourceInstance(
|
||||
instance = resource.instance = createResourceInstance(
|
||||
'link',
|
||||
resource.props,
|
||||
getDocumentFromRoot(root),
|
||||
@@ -838,16 +984,15 @@ function acquireStyleResource(resource: StyleResource): Instance {
|
||||
|
||||
attachLoadListeners(instance, resource);
|
||||
insertStyleInstance(instance, precedence, root);
|
||||
resource.instance = instance;
|
||||
}
|
||||
}
|
||||
resource.count++;
|
||||
// $FlowFixMe[incompatible-return] found when upgrading Flow
|
||||
return resource.instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
function acquireScriptResource(resource: ScriptResource): Instance {
|
||||
if (!resource.instance) {
|
||||
let instance = resource.instance;
|
||||
if (!instance) {
|
||||
const {props, root} = resource;
|
||||
const limitedEscapedSrc = escapeSelectorAttributeValueInsideDoubleQuotes(
|
||||
props.src,
|
||||
@@ -856,19 +1001,19 @@ function acquireScriptResource(resource: ScriptResource): Instance {
|
||||
`script[async][src="${limitedEscapedSrc}"]`,
|
||||
);
|
||||
if (existingEl) {
|
||||
resource.instance = existingEl;
|
||||
instance = resource.instance = existingEl;
|
||||
markNodeAsResource(instance);
|
||||
} else {
|
||||
const instance = createResourceInstance(
|
||||
instance = resource.instance = createResourceInstance(
|
||||
'script',
|
||||
resource.props,
|
||||
getDocumentFromRoot(root),
|
||||
);
|
||||
|
||||
insertResourceInstance(instance, getDocumentFromRoot(root));
|
||||
resource.instance = instance;
|
||||
appendResourceInstance(instance, getDocumentFromRoot(root));
|
||||
}
|
||||
}
|
||||
return resource.instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
function attachLoadListeners(instance: Instance, resource: StyleResource) {
|
||||
@@ -968,14 +1113,38 @@ function insertStyleInstance(
|
||||
}
|
||||
}
|
||||
|
||||
function insertResourceInstance(
|
||||
function prependResourceInstance(
|
||||
instance: Instance,
|
||||
ownerDocument: Document,
|
||||
): void {
|
||||
if (__DEV__) {
|
||||
if (instance.tagName === 'LINK' && (instance: any).rel === 'stylesheet') {
|
||||
console.error(
|
||||
'insertResourceInstance was called with a stylesheet. Stylesheets must be' +
|
||||
'prependResourceInstance was called with a stylesheet. Stylesheets must be' +
|
||||
' inserted with insertStyleInstance instead. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const parent = ownerDocument.head;
|
||||
if (parent) {
|
||||
parent.insertBefore(instance, parent.firstChild);
|
||||
} else {
|
||||
throw new Error(
|
||||
'While attempting to insert a Resource, React expected the Document to contain' +
|
||||
' a head element but it was not found.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function appendResourceInstance(
|
||||
instance: Instance,
|
||||
ownerDocument: Document,
|
||||
): void {
|
||||
if (__DEV__) {
|
||||
if (instance.tagName === 'LINK' && (instance: any).rel === 'stylesheet') {
|
||||
console.error(
|
||||
'appendResourceInstance was called with a stylesheet. Stylesheets must be' +
|
||||
' inserted with insertStyleInstance instead. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
@@ -993,6 +1162,9 @@ function insertResourceInstance(
|
||||
|
||||
export function isHostResourceType(type: string, props: Props): boolean {
|
||||
switch (type) {
|
||||
case 'title': {
|
||||
return true;
|
||||
}
|
||||
case 'link': {
|
||||
switch (props.rel) {
|
||||
case 'stylesheet': {
|
||||
|
||||
+46
-40
@@ -25,6 +25,7 @@ import {
|
||||
getInstanceFromNode as getInstanceFromNodeDOMTree,
|
||||
isContainerMarkedAsRoot,
|
||||
detachDeletedInstance,
|
||||
isMarkedResource,
|
||||
} from './ReactDOMComponentTree';
|
||||
export {detachDeletedInstance};
|
||||
import {hasRole} from './DOMAccessibilityRoles';
|
||||
@@ -58,6 +59,7 @@ import {
|
||||
TEXT_NODE,
|
||||
COMMENT_NODE,
|
||||
DOCUMENT_NODE,
|
||||
DOCUMENT_TYPE_NODE,
|
||||
DOCUMENT_FRAGMENT_NODE,
|
||||
} from '../shared/HTMLNodeType';
|
||||
import dangerousStyleValue from '../shared/dangerousStyleValue';
|
||||
@@ -711,50 +713,15 @@ export function unhideTextInstance(
|
||||
|
||||
export function clearContainer(container: Container): void {
|
||||
if (enableHostSingletons) {
|
||||
// We have refined the container to Element type
|
||||
const nodeType = container.nodeType;
|
||||
if (nodeType === DOCUMENT_NODE || nodeType === ELEMENT_NODE) {
|
||||
switch (container.nodeName) {
|
||||
case '#document':
|
||||
case 'HTML':
|
||||
case 'HEAD':
|
||||
case 'BODY': {
|
||||
let node = container.firstChild;
|
||||
while (node) {
|
||||
const nextNode = node.nextSibling;
|
||||
const nodeName = node.nodeName;
|
||||
switch (nodeName) {
|
||||
case 'HTML':
|
||||
case 'HEAD':
|
||||
case 'BODY': {
|
||||
clearContainer((node: any));
|
||||
// If these singleton instances had previously been rendered with React they
|
||||
// may still hold on to references to the previous fiber tree. We detatch them
|
||||
// prospectiveyl to reset them to a baseline starting state since we cannot create
|
||||
// new instances.
|
||||
detachDeletedInstance((node: any));
|
||||
break;
|
||||
}
|
||||
case 'STYLE': {
|
||||
break;
|
||||
}
|
||||
case 'LINK': {
|
||||
if (
|
||||
((node: any): HTMLLinkElement).rel.toLowerCase() ===
|
||||
'stylesheet'
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
default: {
|
||||
container.removeChild(node);
|
||||
}
|
||||
}
|
||||
node = nextNode;
|
||||
}
|
||||
case 'BODY':
|
||||
clearContainerChildren(container);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
container.textContent = '';
|
||||
}
|
||||
@@ -775,6 +742,42 @@ export function clearContainer(container: Container): void {
|
||||
}
|
||||
}
|
||||
|
||||
function clearContainerChildren(container: Node) {
|
||||
let node;
|
||||
let nextNode: ?Node = container.firstChild;
|
||||
if (nextNode && nextNode.nodeType === DOCUMENT_TYPE_NODE) {
|
||||
nextNode = nextNode.nextSibling;
|
||||
}
|
||||
while (nextNode) {
|
||||
node = nextNode;
|
||||
nextNode = nextNode.nextSibling;
|
||||
switch (node.nodeName) {
|
||||
case 'HTML':
|
||||
case 'HEAD':
|
||||
case 'BODY': {
|
||||
const element: Element = (node: any);
|
||||
clearContainerChildren(element);
|
||||
// If these singleton instances had previously been rendered with React they
|
||||
// may still hold on to references to the previous fiber tree. We detatch them
|
||||
// prospectively to reset them to a baseline starting state since we cannot create
|
||||
// new instances.
|
||||
detachDeletedInstance(element);
|
||||
continue;
|
||||
}
|
||||
case 'STYLE': {
|
||||
continue;
|
||||
}
|
||||
case 'LINK': {
|
||||
if (((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
container.removeChild(node);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Making this so we can eventually move all of the instance caching to the commit phase.
|
||||
// Currently this is only used to associate fiber and props to instances for hydrating
|
||||
// HostSingletons. The reason we need it here is we only want to make this binding on commit
|
||||
@@ -923,6 +926,7 @@ function getNextHydratable(node) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'TITLE':
|
||||
case 'HTML':
|
||||
case 'HEAD':
|
||||
case 'BODY': {
|
||||
@@ -948,6 +952,9 @@ function getNextHydratable(node) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'TITLE': {
|
||||
continue;
|
||||
}
|
||||
case 'STYLE': {
|
||||
const styleEl: HTMLStyleElement = (element: any);
|
||||
if (styleEl.hasAttribute('data-precedence')) {
|
||||
@@ -1666,9 +1673,8 @@ export function clearSingleton(instance: Instance): void {
|
||||
while (node) {
|
||||
const nextNode = node.nextSibling;
|
||||
const nodeName = node.nodeName;
|
||||
if (getInstanceFromNodeDOMTree(node)) {
|
||||
// retain nodes owned by React
|
||||
} else if (
|
||||
if (
|
||||
isMarkedResource(node) ||
|
||||
nodeName === 'HEAD' ||
|
||||
nodeName === 'BODY' ||
|
||||
nodeName === 'STYLE' ||
|
||||
|
||||
@@ -68,6 +68,18 @@ type ScriptResource = {
|
||||
hint: PreloadResource,
|
||||
};
|
||||
|
||||
type HeadProps = {
|
||||
[string]: mixed,
|
||||
};
|
||||
type HeadResource = {
|
||||
type: 'head',
|
||||
instanceType: string,
|
||||
props: HeadProps,
|
||||
|
||||
flushed: boolean,
|
||||
allowLate: boolean,
|
||||
};
|
||||
|
||||
export type Resource = PreloadResource | StyleResource | ScriptResource;
|
||||
|
||||
export type Resources = {
|
||||
@@ -75,8 +87,10 @@ export type Resources = {
|
||||
preloadsMap: Map<string, PreloadResource>,
|
||||
stylesMap: Map<string, StyleResource>,
|
||||
scriptsMap: Map<string, ScriptResource>,
|
||||
headsMap: Map<string, HeadResource>,
|
||||
|
||||
// Flushing queues for Resource dependencies
|
||||
charset: null | HeadResource,
|
||||
fontPreloads: Set<PreloadResource>,
|
||||
// usedImagePreloads: Set<PreloadResource>,
|
||||
precedences: Map<string, Set<StyleResource>>,
|
||||
@@ -86,6 +100,7 @@ export type Resources = {
|
||||
explicitStylePreloads: Set<PreloadResource>,
|
||||
// explicitImagePreloads: Set<PreloadResource>,
|
||||
explicitScriptPreloads: Set<PreloadResource>,
|
||||
headResources: Set<HeadResource>,
|
||||
|
||||
// Module-global-like reference for current boundary resources
|
||||
boundaryResources: ?BoundaryResources,
|
||||
@@ -99,8 +114,10 @@ export function createResources(): Resources {
|
||||
preloadsMap: new Map(),
|
||||
stylesMap: new Map(),
|
||||
scriptsMap: new Map(),
|
||||
headsMap: new Map(),
|
||||
|
||||
// cleared on flush
|
||||
charset: null,
|
||||
fontPreloads: new Set(),
|
||||
// usedImagePreloads: new Set(),
|
||||
precedences: new Map(),
|
||||
@@ -110,6 +127,7 @@ export function createResources(): Resources {
|
||||
explicitStylePreloads: new Set(),
|
||||
// explicitImagePreloads: new Set(),
|
||||
explicitScriptPreloads: new Set(),
|
||||
headResources: new Set(),
|
||||
|
||||
// like a module global for currently rendering boundary
|
||||
boundaryResources: null,
|
||||
@@ -563,6 +581,74 @@ function adoptPreloadPropsForScriptProps(
|
||||
resourceProps.integrity = preloadProps.integrity;
|
||||
}
|
||||
|
||||
function createHeadResource(
|
||||
resources: Resources,
|
||||
key: string,
|
||||
instanceType: string,
|
||||
props: HeadProps,
|
||||
): HeadResource {
|
||||
if (__DEV__) {
|
||||
if (resources.headsMap.has(key)) {
|
||||
console.error(
|
||||
'createScriptResource was called when a script Resource matching the same src already exists. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const resource: HeadResource = {
|
||||
type: 'head',
|
||||
instanceType,
|
||||
props,
|
||||
|
||||
flushed: false,
|
||||
allowLate: true,
|
||||
};
|
||||
resources.headsMap.set(key, resource);
|
||||
return resource;
|
||||
}
|
||||
|
||||
function getTitleKey(child: string | number): string {
|
||||
return 'title' + child;
|
||||
}
|
||||
|
||||
function titlePropsFromRawProps(
|
||||
child: string | number,
|
||||
rawProps: Props,
|
||||
): HeadProps {
|
||||
const props = Object.assign({}, rawProps);
|
||||
props.children = child;
|
||||
return props;
|
||||
}
|
||||
|
||||
export function resourcesFromElement(type: string, props: Props): boolean {
|
||||
if (!currentResources) {
|
||||
throw new Error(
|
||||
'"currentResources" was expected to exist. This is a bug in React.',
|
||||
);
|
||||
}
|
||||
const resources = currentResources;
|
||||
switch (type) {
|
||||
case 'title': {
|
||||
let child = props.children;
|
||||
if (Array.isArray(child) && child.length === 1) {
|
||||
child = child[0];
|
||||
}
|
||||
if (typeof child === 'string' || typeof child === 'number') {
|
||||
const key = getTitleKey(child);
|
||||
let resource = resources.headsMap.get(key);
|
||||
if (!resource) {
|
||||
const titleProps = titlePropsFromRawProps(child, props);
|
||||
resource = createHeadResource(resources, key, 'title', titleProps);
|
||||
resources.headResources.add(resource);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Construct a resource from link props.
|
||||
export function resourcesFromLink(props: Props): boolean {
|
||||
if (!currentResources) {
|
||||
|
||||
@@ -64,6 +64,7 @@ import isArray from 'shared/isArray';
|
||||
import {
|
||||
prepareToRenderResources,
|
||||
finishRenderingResources,
|
||||
resourcesFromElement,
|
||||
resourcesFromLink,
|
||||
resourcesFromScript,
|
||||
ReactDOMServerDispatcher,
|
||||
@@ -1277,6 +1278,20 @@ function pushStartTitle(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
props: Object,
|
||||
responseState: ResponseState,
|
||||
): ReactNodeList {
|
||||
if (enableFloat && resourcesFromElement('title', props)) {
|
||||
// We have converted this link exclusively to a resource and no longer
|
||||
// need to emit it
|
||||
return null;
|
||||
}
|
||||
|
||||
return pushStartTitleImpl(target, props, responseState);
|
||||
}
|
||||
|
||||
function pushStartTitleImpl(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
props: Object,
|
||||
responseState: ResponseState,
|
||||
): ReactNodeList {
|
||||
target.push(startChunkForTag('title'));
|
||||
|
||||
@@ -2310,6 +2325,7 @@ export function writeInitialResources(
|
||||
usedScriptPreloads,
|
||||
explicitStylePreloads,
|
||||
explicitScriptPreloads,
|
||||
headResources,
|
||||
} = resources;
|
||||
|
||||
fontPreloads.forEach(r => {
|
||||
@@ -2360,6 +2376,18 @@ export function writeInitialResources(
|
||||
explicitScriptPreloads.forEach(flushLinkResource);
|
||||
explicitScriptPreloads.clear();
|
||||
|
||||
headResources.forEach(r => {
|
||||
if (r.instanceType === 'title') {
|
||||
pushStartTitleImpl(target, r.props, responseState);
|
||||
if (typeof r.props.children === 'string') {
|
||||
target.push(escapeTextForBrowser(stringToChunk(r.props.children)));
|
||||
}
|
||||
pushEndInstance(target, target, 'title', r.props);
|
||||
}
|
||||
r.flushed = true;
|
||||
});
|
||||
headResources.clear();
|
||||
|
||||
let i;
|
||||
let r = true;
|
||||
for (i = 0; i < target.length - 1; i++) {
|
||||
@@ -2392,6 +2420,7 @@ export function writeImmediateResources(
|
||||
usedScriptPreloads,
|
||||
explicitStylePreloads,
|
||||
explicitScriptPreloads,
|
||||
headResources,
|
||||
} = resources;
|
||||
|
||||
fontPreloads.forEach(r => {
|
||||
@@ -2422,6 +2451,18 @@ export function writeImmediateResources(
|
||||
explicitScriptPreloads.forEach(flushLinkResource);
|
||||
explicitScriptPreloads.clear();
|
||||
|
||||
headResources.forEach(r => {
|
||||
if (r.instanceType === 'title') {
|
||||
pushStartTitle(target, r.props, responseState);
|
||||
if (typeof r.props.children === 'string') {
|
||||
target.push(escapeTextForBrowser(stringToChunk(r.props.children)));
|
||||
}
|
||||
pushEndInstance(target, target, 'title', r.props);
|
||||
}
|
||||
r.flushed = true;
|
||||
});
|
||||
headResources.clear();
|
||||
|
||||
let i;
|
||||
let r = true;
|
||||
for (i = 0; i < target.length - 1; i++) {
|
||||
|
||||
@@ -15,4 +15,5 @@ export const ELEMENT_NODE = 1;
|
||||
export const TEXT_NODE = 3;
|
||||
export const COMMENT_NODE = 8;
|
||||
export const DOCUMENT_NODE = 9;
|
||||
export const DOCUMENT_TYPE_NODE = 10;
|
||||
export const DOCUMENT_FRAGMENT_NODE = 11;
|
||||
|
||||
+21
-12
@@ -4968,18 +4968,27 @@ describe('ReactDOMFizzServer', () => {
|
||||
},
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield([]);
|
||||
expect(errors).toEqual(
|
||||
[
|
||||
gate(flags => flags.enableClientRenderFallbackOnTextMismatch)
|
||||
? 'Text content does not match server-rendered HTML.'
|
||||
: null,
|
||||
'Hydration failed because the initial UI does not match what was rendered on the server.',
|
||||
'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
|
||||
].filter(Boolean),
|
||||
);
|
||||
expect(getVisibleChildren(container)).toEqual(
|
||||
<title>{['hello1', 'hello2']}</title>,
|
||||
);
|
||||
if (gate(flags => flags.enableFloat)) {
|
||||
expect(errors).toEqual([]);
|
||||
// with float, the title doesn't render on the client because it is not a simple child
|
||||
// we end up seeing the server rendered title
|
||||
expect(getVisibleChildren(container)).toEqual(
|
||||
<title>{'hello1<!-- -->hello2'}</title>,
|
||||
);
|
||||
} else {
|
||||
expect(errors).toEqual(
|
||||
[
|
||||
gate(flags => flags.enableClientRenderFallbackOnTextMismatch)
|
||||
? 'Text content does not match server-rendered HTML.'
|
||||
: null,
|
||||
'Hydration failed because the initial UI does not match what was rendered on the server.',
|
||||
'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
|
||||
].filter(Boolean),
|
||||
);
|
||||
expect(getVisibleChildren(container)).toEqual(
|
||||
<title>{['hello1', 'hello2']}</title>,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
console.error = originalConsoleError;
|
||||
}
|
||||
|
||||
@@ -235,6 +235,80 @@ describe('ReactDOMFloat', () => {
|
||||
return readText(text);
|
||||
}
|
||||
|
||||
// @gate enableFloat
|
||||
it('can render resources before singletons', async () => {
|
||||
const root = ReactDOMClient.createRoot(document);
|
||||
root.render(
|
||||
<>
|
||||
<title>foo</title>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="foo" href="foo" />
|
||||
</head>
|
||||
<body>hello world</body>
|
||||
</html>
|
||||
</>,
|
||||
);
|
||||
try {
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
} catch (e) {
|
||||
// for DOMExceptions that happen when expecting this test to fail we need
|
||||
// to clear the scheduler first otherwise the expected failure will fail
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
throw e;
|
||||
}
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<title>foo</title>
|
||||
<link rel="foo" href="foo" />
|
||||
</head>
|
||||
<body>hello world</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableFloat
|
||||
it('can acquire a resource after releasing it in the same commit', async () => {
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
root.render(
|
||||
<>
|
||||
<title>foo</title>
|
||||
</>,
|
||||
);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<title>foo</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container" />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
// title is keyed off children so this second resource should match the first one
|
||||
root.render(
|
||||
<>
|
||||
{null}
|
||||
<title data-new="new">foo</title>
|
||||
</>,
|
||||
);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
// we don't see the attribute because the resource is the same and was not reconstructed
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<title>foo</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container" />
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableFloat
|
||||
it('errors if the document does not contain a head when inserting a resource', async () => {
|
||||
document.head.parentNode.removeChild(document.head);
|
||||
@@ -866,6 +940,261 @@ describe('ReactDOMFloat', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('head resources', () => {
|
||||
// @gate enableFloat
|
||||
it('can rendering title tags anywhere in the tree', async () => {
|
||||
await actIntoEmptyDocument(() => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
|
||||
<>
|
||||
<title>before</title>
|
||||
<>
|
||||
<html>
|
||||
<head>
|
||||
<title>in head</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<title>during</title>
|
||||
hello world
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
<title>after</title>
|
||||
</>,
|
||||
);
|
||||
pipe(writable);
|
||||
});
|
||||
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<title>before</title>
|
||||
<title>in head</title>
|
||||
<title>during</title>
|
||||
<title>after</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>hello world</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
ReactDOMClient.hydrateRoot(
|
||||
document,
|
||||
<>
|
||||
<title>before</title>
|
||||
<>
|
||||
<html>
|
||||
<head>
|
||||
<title>in head</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<title>during</title>
|
||||
hello world
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</>
|
||||
<title>after</title>
|
||||
</>,
|
||||
);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<title>before</title>
|
||||
<title>in head</title>
|
||||
<title>during</title>
|
||||
<title>after</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>hello world</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableFloat
|
||||
it('prepends new titles on the client so newer ones override older ones, including orphaned server rendered titles', async () => {
|
||||
await actIntoEmptyDocument(() => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
|
||||
<html>
|
||||
<head>
|
||||
<title>server</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>hello world</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
pipe(writable);
|
||||
});
|
||||
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<title>server</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>hello world</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
ReactDOMClient.hydrateRoot(
|
||||
document,
|
||||
<html>
|
||||
<title>html</title>
|
||||
<head>
|
||||
<title>head</title>
|
||||
</head>
|
||||
<body>
|
||||
<title>body</title>
|
||||
<div>hello world</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<title>body</title>
|
||||
<title>head</title>
|
||||
<title>html</title>
|
||||
<title>server</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>hello world</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableFloat
|
||||
it('keys titles on text children and only removes them when no more instances refer to that title', async () => {
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
root.render(
|
||||
<div>
|
||||
<title>{[2]}</title>hello world<title>2</title>
|
||||
</div>,
|
||||
);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<title>2</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div>hello world</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
root.render(
|
||||
<div>
|
||||
{null}hello world<title>2</title>
|
||||
</div>,
|
||||
);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<title>2</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div>hello world</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
root.render(
|
||||
<div>
|
||||
{null}hello world{null}
|
||||
</div>,
|
||||
);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head />
|
||||
<body>
|
||||
<div id="container">
|
||||
<div>hello world</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableFloat
|
||||
it('can render a title before a singleton even if that singleton clears its contents', async () => {
|
||||
await actIntoEmptyDocument(() => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
|
||||
<>
|
||||
<title>foo</title>
|
||||
<html>
|
||||
<head />
|
||||
<body>
|
||||
<div>server</div>
|
||||
</body>
|
||||
</html>
|
||||
,
|
||||
</>,
|
||||
);
|
||||
pipe(writable);
|
||||
});
|
||||
|
||||
const errors = [];
|
||||
ReactDOMClient.hydrateRoot(
|
||||
document,
|
||||
<>
|
||||
<title>foo</title>
|
||||
<html>
|
||||
<head />
|
||||
<body>
|
||||
<div>client</div>
|
||||
</body>
|
||||
</html>
|
||||
</>,
|
||||
{
|
||||
onRecoverableError(err) {
|
||||
errors.push(err.message);
|
||||
},
|
||||
},
|
||||
);
|
||||
try {
|
||||
expect(() => {
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
}).toErrorDev(
|
||||
[
|
||||
'Warning: Text content did not match. Server: "server" Client: "client"',
|
||||
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
|
||||
],
|
||||
{withoutStack: 1},
|
||||
);
|
||||
} catch (e) {
|
||||
// When gates are false this test fails on a DOMException if you don't clear the scheduler after catching.
|
||||
// When gates are true this branch should not be hit
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
throw e;
|
||||
}
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<title>foo</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>client</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('style resources', () => {
|
||||
// @gate enableFloat
|
||||
it('treats link rel stylesheet elements as a style resource when it includes a precedence when server rendering', async () => {
|
||||
@@ -1229,6 +1558,7 @@ describe('ReactDOMFloat', () => {
|
||||
<link rel="stylesheet" href="foo" data-precedence="foo" />
|
||||
<link rel="stylesheet" href="bar" data-precedence="bar" />
|
||||
<link rel="stylesheet" href="baz" data-precedence="baz" />
|
||||
<link rel="preload" href="baz" as="style" />
|
||||
</head>
|
||||
<body>hello</body>
|
||||
</html>,
|
||||
|
||||
@@ -155,8 +155,8 @@ describe('ReactDOM HostSingleton', () => {
|
||||
expect(getVisibleChildren(document)).toEqual(
|
||||
<html>
|
||||
<head lang="es" data-foo="foo">
|
||||
<title>Hello</title>
|
||||
<title>Hola</title>
|
||||
<title>Hello</title>
|
||||
</head>
|
||||
<body />
|
||||
</html>,
|
||||
@@ -241,8 +241,8 @@ describe('ReactDOM HostSingleton', () => {
|
||||
<link rel="preload" href="resource" as="style" />
|
||||
<link rel="preload" href="3rdparty" as="style" />
|
||||
<link rel="preload" href="3rdparty2" as="style" />
|
||||
<link rel="stylesheet" href="resource" />
|
||||
<title>a server title</title>
|
||||
<link rel="stylesheet" href="resource" />
|
||||
<link rel="stylesheet" href="3rdparty" />
|
||||
<link rel="stylesheet" href="3rdparty2" />
|
||||
</head>
|
||||
@@ -287,10 +287,10 @@ describe('ReactDOM HostSingleton', () => {
|
||||
expect(getVisibleChildren(document)).toEqual(
|
||||
<html data-client-foo="foo">
|
||||
<head>
|
||||
<title>a client title</title>
|
||||
<link rel="stylesheet" href="resource" />
|
||||
<link rel="stylesheet" href="3rdparty" />
|
||||
<link rel="stylesheet" href="3rdparty2" />
|
||||
<title>a client title</title>
|
||||
</head>
|
||||
<body data-client-baz="baz">
|
||||
<style>
|
||||
@@ -326,10 +326,10 @@ describe('ReactDOM HostSingleton', () => {
|
||||
expect(getVisibleChildren(document)).toEqual(
|
||||
<html data-client-foo="foo">
|
||||
<head>
|
||||
<title>a client title</title>
|
||||
<link rel="stylesheet" href="resource" />
|
||||
<link rel="stylesheet" href="3rdparty" />
|
||||
<link rel="stylesheet" href="3rdparty2" />
|
||||
<title>a client title</title>
|
||||
<meta />
|
||||
</head>
|
||||
<body data-client-baz="baz">
|
||||
@@ -365,10 +365,10 @@ describe('ReactDOM HostSingleton', () => {
|
||||
expect(getVisibleChildren(document)).toEqual(
|
||||
<html data-client-foo="foo">
|
||||
<head>
|
||||
<title>a client title</title>
|
||||
<link rel="stylesheet" href="resource" />
|
||||
<link rel="stylesheet" href="3rdparty" />
|
||||
<link rel="stylesheet" href="3rdparty2" />
|
||||
<title>a client title</title>
|
||||
</head>
|
||||
<body data-client-baz="baz">
|
||||
<style>
|
||||
@@ -401,10 +401,10 @@ describe('ReactDOM HostSingleton', () => {
|
||||
expect(getVisibleChildren(document)).toEqual(
|
||||
<html data-client-foo="foo">
|
||||
<head>
|
||||
<title>a client title</title>
|
||||
<link rel="stylesheet" href="resource" />
|
||||
<link rel="stylesheet" href="3rdparty" />
|
||||
<link rel="stylesheet" href="3rdparty2" />
|
||||
<title>a client title</title>
|
||||
</head>
|
||||
<body>
|
||||
<style>
|
||||
@@ -472,19 +472,15 @@ describe('ReactDOM HostSingleton', () => {
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
}).toErrorDev(
|
||||
[
|
||||
`Warning: Expected server HTML to contain a matching <title> in <head>.
|
||||
in title (at **)
|
||||
in head (at **)
|
||||
`Warning: Expected server HTML to contain a matching <div> in <body>.
|
||||
in div (at **)
|
||||
in body (at **)
|
||||
in html (at **)`,
|
||||
`Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.`,
|
||||
],
|
||||
{withoutStack: 1},
|
||||
);
|
||||
expect(hydrationErrors).toEqual([
|
||||
[
|
||||
'Hydration failed because the initial UI does not match what was rendered on the server.',
|
||||
'at title',
|
||||
],
|
||||
[
|
||||
'Hydration failed because the initial UI does not match what was rendered on the server.',
|
||||
'at div',
|
||||
@@ -502,10 +498,10 @@ describe('ReactDOM HostSingleton', () => {
|
||||
expect(getVisibleChildren(document)).toEqual(
|
||||
<html data-client-foo="foo">
|
||||
<head>
|
||||
<title>a client title</title>
|
||||
<link rel="stylesheet" href="resource" />
|
||||
<link rel="stylesheet" href="3rdparty" />
|
||||
<link rel="stylesheet" href="3rdparty2" />
|
||||
<title>a client title</title>
|
||||
</head>
|
||||
<body data-client-baz="baz">
|
||||
<style>
|
||||
@@ -768,9 +764,9 @@ describe('ReactDOM HostSingleton', () => {
|
||||
expect(getVisibleChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<title>something new</title>
|
||||
<link rel="stylesheet" href="headbefore" />
|
||||
<link rel="stylesheet" href="headafter" />
|
||||
<title>something new</title>
|
||||
</head>
|
||||
<body>
|
||||
<link rel="stylesheet" href="bodybefore" />
|
||||
@@ -804,9 +800,9 @@ describe('ReactDOM HostSingleton', () => {
|
||||
expect(getVisibleChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<title>something new</title>
|
||||
<link rel="stylesheet" href="before" />
|
||||
<link rel="stylesheet" href="after" />
|
||||
<title>something new</title>
|
||||
</head>
|
||||
<body />
|
||||
</html>,
|
||||
@@ -979,4 +975,37 @@ describe('ReactDOM HostSingleton', () => {
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableHostSingletons
|
||||
it('allows for hydrating without a head', async () => {
|
||||
await actIntoEmptyDocument(() => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
|
||||
<html>
|
||||
<body>foo</body>
|
||||
</html>,
|
||||
);
|
||||
pipe(writable);
|
||||
});
|
||||
|
||||
expect(getVisibleChildren(document)).toEqual(
|
||||
<html>
|
||||
<head />
|
||||
<body>foo</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
ReactDOMClient.hydrateRoot(
|
||||
document,
|
||||
<html>
|
||||
<body>foo</body>
|
||||
</html>,
|
||||
);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(getVisibleChildren(document)).toEqual(
|
||||
<html>
|
||||
<head />
|
||||
<body>foo</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -253,10 +253,21 @@ describe('rendering React components at document', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// getTestDocument() has an extra <meta> that we didn't render.
|
||||
expect(() =>
|
||||
ReactDOM.hydrate(<Component text="Hello world" />, testDocument),
|
||||
).toErrorDev('Did not expect server HTML to contain a <meta> in <head>.');
|
||||
if (gate(flags => flags.enableFloat)) {
|
||||
// with float the title no longer is a hydration mismatch so we get an error on the body mismatch
|
||||
expect(() =>
|
||||
ReactDOM.hydrate(<Component text="Hello world" />, testDocument),
|
||||
).toErrorDev(
|
||||
'Expected server HTML to contain a matching text node for "Hello world" in <body>',
|
||||
);
|
||||
} else {
|
||||
// getTestDocument() has an extra <meta> that we didn't render.
|
||||
expect(() =>
|
||||
ReactDOM.hydrate(<Component text="Hello world" />, testDocument),
|
||||
).toErrorDev(
|
||||
'Did not expect server HTML to contain a <meta> in <head>.',
|
||||
);
|
||||
}
|
||||
expect(testDocument.body.innerHTML).toBe('Hello world');
|
||||
});
|
||||
|
||||
|
||||
@@ -1599,13 +1599,13 @@ function updateHostResource(current, workInProgress, renderLanes) {
|
||||
workInProgress.pendingProps,
|
||||
currentProps,
|
||||
);
|
||||
reconcileChildren(
|
||||
current,
|
||||
workInProgress,
|
||||
workInProgress.pendingProps.children,
|
||||
renderLanes,
|
||||
);
|
||||
return workInProgress.child;
|
||||
// Resources never have reconciler managed children. It is possible for
|
||||
// the host implementation of getResource to consider children in the
|
||||
// resource construction but they will otherwise be discarded. In practice
|
||||
// this precludes all but the simplest children and Host specific warnings
|
||||
// should be implemented to warn when children are passsed when otherwise not
|
||||
// expected
|
||||
return null;
|
||||
}
|
||||
|
||||
function updateHostSingleton(
|
||||
|
||||
@@ -1599,13 +1599,13 @@ function updateHostResource(current, workInProgress, renderLanes) {
|
||||
workInProgress.pendingProps,
|
||||
currentProps,
|
||||
);
|
||||
reconcileChildren(
|
||||
current,
|
||||
workInProgress,
|
||||
workInProgress.pendingProps.children,
|
||||
renderLanes,
|
||||
);
|
||||
return workInProgress.child;
|
||||
// Resources never have reconciler managed children. It is possible for
|
||||
// the host implementation of getResource to consider children in the
|
||||
// resource construction but they will otherwise be discarded. In practice
|
||||
// this precludes all but the simplest children and Host specific warnings
|
||||
// should be implemented to warn when children are passsed when otherwise not
|
||||
// expected
|
||||
return null;
|
||||
}
|
||||
|
||||
function updateHostSingleton(
|
||||
|
||||
Reference in New Issue
Block a user