Skip to content

Commit

Permalink
support hydration properly
Browse files Browse the repository at this point in the history
  • Loading branch information
gnoff committed Oct 6, 2022
1 parent 101c086 commit 46787fd
Show file tree
Hide file tree
Showing 19 changed files with 155 additions and 223 deletions.
15 changes: 2 additions & 13 deletions packages/react-dom-bindings/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,8 @@ function setInitialDOMProperties(
// textContent on a <textarea> will cause the placeholder to not
// show within the <textarea> until it has been focused and blurred again.
// https://github.com/facebook/react/issues/6731#issuecomment-254874553
const canSetTextContent = tag !== 'textarea' || nextProp !== '';
const canSetTextContent =
tag !== 'body' && (tag !== 'textarea' || nextProp !== '');
if (canSetTextContent) {
setTextContent(domElement, nextProp);
}
Expand Down Expand Up @@ -483,18 +484,6 @@ export function createTextNode(
);
}

export function resetProperties(
domElement: Element,
tag: string,
rawProps: Object,
): void {
const attributes = domElement.attributes;
while (attributes.length) {
domElement.removeAttribute(attributes[0].name);
}
setInitialProperties(domElement, tag, rawProps);
}

export function setInitialProperties(
domElement: Element,
tag: string,
Expand Down
11 changes: 3 additions & 8 deletions packages/react-dom-bindings/src/client/ReactDOMComponentTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@ import {
SuspenseComponent,
} from 'react-reconciler/src/ReactWorkTags';

import {
getParentSuspenseInstance,
supportsSingletons,
} from './ReactDOMHostConfig';
import {getParentSuspenseInstance} from './ReactDOMHostConfig';

import {
enableScopeAPI,
Expand Down Expand Up @@ -182,9 +179,7 @@ export function getInstanceFromNode(node: Node): Fiber | null {
tag === SuspenseComponent ||
tag === HostRoot ||
(enableFloat ? tag === HostResource : false) ||
(enableHostSingletons && supportsSingletons
? tag === HostSingleton
: false)
(enableHostSingletons ? tag === HostSingleton : false)
) {
return inst;
} else {
Expand All @@ -204,7 +199,7 @@ export function getNodeFromInstance(inst: Fiber): Instance | TextInstance {
tag === HostComponent ||
tag === HostText ||
(enableFloat ? tag === HostResource : false) ||
(enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
(enableHostSingletons ? tag === HostSingleton : false)
) {
// In Fiber this, is just the state node right now. We assume it will be
// a host component or host text.
Expand Down
112 changes: 47 additions & 65 deletions packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import {
setInitialProperties,
diffProperties,
updateProperties,
resetProperties,
diffHydratedProperties,
diffHydratedText,
trapClickOnNonInteractiveElement,
Expand Down Expand Up @@ -124,7 +123,6 @@ export type Container =
| interface extends Document {_reactRootContainer?: FiberRoot}
| interface extends DocumentFragment {_reactRootContainer?: FiberRoot};
export type Instance = Element;
export type InstanceSibling = Node;
export type TextInstance = Text;
export interface SuspenseInstance extends Comment {
_reactRetry?: () => void;
Expand Down Expand Up @@ -552,15 +550,15 @@ export function appendChildToContainer(
export function insertBefore(
parentInstance: Instance,
child: Instance | TextInstance,
beforeChild: InstanceSibling,
beforeChild: Instance,
): void {
parentInstance.insertBefore(child, beforeChild);
}

export function insertInContainerBefore(
container: Container,
child: Instance | TextInstance,
beforeChild: InstanceSibling,
beforeChild: Instance,
): void {
if (container.nodeType === COMMENT_NODE) {
(container.parentNode: any).insertBefore(child, beforeChild);
Expand Down Expand Up @@ -717,15 +715,32 @@ export function clearContainer(container: Container): void {
case 'html':
case 'head':
case 'body': {
clearSingletonInstance(element);
break;
let node = element.firstChild;
while (node) {
const nextNode = node.nextSibling;
const nodeName = node.nodeName;
if (
nodeName === 'HEAD' ||
nodeName === 'BODY' ||
nodeName === 'STYLE' ||
(nodeName === 'LINK' &&
((node: any): HTMLLinkElement).rel.toLowerCase() ===
'stylesheet')
) {
// retain these nodes
} else {
element.removeChild(node);
}
node = nextNode;
}
return;
}
default: {
element.textContent = '';
}
}
}
// Implicitly if the container is of type Document we rely on the Persistent HostComponent
// Implicitly if the container is of type Document we rely on the HostSingleton
// semantics to clear these nodes appropriately when being placed so no ahead of time
// clearing is necessary
} else {
Expand Down Expand Up @@ -1246,7 +1261,7 @@ export function errorHydratingContainer(parentContainer: Container): void {
}
}

export function acquireSingletonInstance(
export function resolveSingletonInstance(
type: string,
props: Props,
rootContainerInstance: Container,
Expand All @@ -1262,50 +1277,51 @@ export function acquireSingletonInstance(
const ownerDocument = getOwnerDocumentFromRootContainer(
rootContainerInstance,
);
// For the three persistent Host Components that exist in DOM it is necessary for there to
// always be a documentElement. With normal html parsing this will always be the case but
// with pathological manipulation the document can end up in a state where no documentElement
// exists. We create it here if missing so we can treat it as an invariant.
// It is important to note that this dom mutation and others in this function happen
// in render rather than commit. This is tolerable because they only happen in degenerate cases
let htmlElement = ownerDocument.documentElement;
if (!htmlElement) {
htmlElement = ownerDocument.appendChild(
ownerDocument.createElement('html'),
);
}
switch (type) {
case 'html': {
return htmlElement;
const documentElement = ownerDocument.documentElement;
if (!documentElement) {
throw new Error(
'React expected an <html> element (document.documentElement) to exist in the Document but one was' +
' not found. React never removes the documentElement for any Document it renders into so' +
' the cause is likely in some other script running on this page.',
);
}
return documentElement;
}
case 'head': {
let head = ownerDocument.head;
const head = ownerDocument.head;
if (!head) {
head = htmlElement.insertBefore(
ownerDocument.createElement('head'),
ownerDocument.firstChild,
throw new Error(
'React expected a <head> element (document.head) to exist in the Document but one was' +
' not found. React never removes the head for any Document it renders into so' +
' the cause is likely in some other script running on this page.',
);
}
// We ensure this exists just above
return head;
}
case 'body': {
let body = ownerDocument.body;
const body = ownerDocument.body;
if (!body) {
body = htmlElement.appendChild(ownerDocument.createElement('body'));
throw new Error(
'React expected a <body> element (document.body) to exist in the Document but one was' +
' not found. React never removes the body for any Document it renders into so' +
' the cause is likely in some other script running on this page.',
);
}
// We ensure this exists just above
return body;
}
default: {
throw new Error(
'acquireSingletonInstance was called with an element type that is not supported. This is a bug in React.',
'resolveSingletonInstance was called with an element type that is not supported. This is a bug in React.',
);
}
}
}

export function resetSingletonInstance(
export function acquireSingletonInstance(
type: string,
props: Props,
instance: Instance,
Expand All @@ -1320,51 +1336,17 @@ export function resetSingletonInstance(
}
default: {
console.error(
'resetSingletonInstance was called with an element type that is not supported. This is a bug in React.',
'acquireSingletonInstance was called with an element type that is not supported. This is a bug in React.',
);
}
}
}

clearSingletonInstance(instance);
resetProperties(instance, type, props);
setInitialProperties(instance, type, props);
precacheFiberNode(internalInstanceHandle, instance);
updateFiberProps(instance, props);
}

export function clearSingletonInstance(instance: Instance) {
const tagName = instance.tagName.toLowerCase();
switch (tagName) {
case 'html': {
return;
}
case 'head':
case 'body': {
let node = instance.firstChild;
while (node) {
const nextNode = node.nextSibling;
const nodeName = node.nodeName;
if (
nodeName === 'STYLE' ||
(nodeName === 'LINK' &&
((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet')
) {
// retain these nodes
} else {
instance.removeChild(node);
}
node = nextNode;
}
return;
}
default: {
throw new Error(
'clearSingletonInstance was called with an element type that is not supported. this is a bug in React.',
);
}
}
}

// -------------------
// Test Selectors
// -------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4375,7 +4375,7 @@ describe('ReactDOMFizzServer', () => {
);
});

// @gate enableHostSingletons
// @gate enableFloat
it('holds back body and html closing tags (the postamble) until all pending tasks are completed', async () => {
const chunks = [];
writable.on('data', chunk => {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-dom/src/__tests__/ReactDOMRoot-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ describe('ReactDOMRoot', () => {
);
});

// @gate !__DEV__ || !enableHostSingletons
// @gate !enableFloat && !enableHostSingletons
it('warns if updating a root that has had its contents removed', async () => {
const root = ReactDOMClient.createRoot(container);
root.render(<div>Hi</div>);
Expand Down
Loading

0 comments on commit 46787fd

Please sign in to comment.