Files
react/packages/react-dom/src/client/ReactDOM.js
T

1355 lines
39 KiB
JavaScript

/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ReactNodeList} from 'shared/ReactTypes';
// TODO: This type is shared between the reconciler and ReactDOM, but will
// eventually be lifted out to the renderer.
import type {
FiberRoot,
Batch as FiberRootBatch,
} from 'react-reconciler/src/ReactFiberRoot';
import '../shared/checkReact';
import './ReactDOMClientInjection';
import ReactFiberReconciler from 'react-reconciler';
// TODO: direct imports like some-package/src/* are bad. Fix me.
import * as ReactPortal from 'shared/ReactPortal';
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment';
import * as ReactGenericBatching from 'events/ReactGenericBatching';
import * as ReactControlledComponent from 'events/ReactControlledComponent';
import * as EventPluginHub from 'events/EventPluginHub';
import * as EventPluginRegistry from 'events/EventPluginRegistry';
import * as EventPropagators from 'events/EventPropagators';
import * as ReactInstanceMap from 'shared/ReactInstanceMap';
import {enableCreateRoot} from 'shared/ReactFeatureFlags';
import ReactVersion from 'shared/ReactVersion';
import * as ReactDOMFrameScheduling from 'shared/ReactDOMFrameScheduling';
import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
import getComponentName from 'shared/getComponentName';
import invariant from 'fbjs/lib/invariant';
import lowPriorityWarning from 'shared/lowPriorityWarning';
import warning from 'fbjs/lib/warning';
import * as ReactDOMComponentTree from './ReactDOMComponentTree';
import * as ReactDOMFiberComponent from './ReactDOMFiberComponent';
import * as ReactInputSelection from './ReactInputSelection';
import setTextContent from './setTextContent';
import validateDOMNesting from './validateDOMNesting';
import * as ReactBrowserEventEmitter from '../events/ReactBrowserEventEmitter';
import * as ReactDOMEventListener from '../events/ReactDOMEventListener';
import {getChildNamespace} from '../shared/DOMNamespaces';
import {
ELEMENT_NODE,
TEXT_NODE,
COMMENT_NODE,
DOCUMENT_NODE,
DOCUMENT_FRAGMENT_NODE,
} from '../shared/HTMLNodeType';
import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty';
const {
createElement,
createTextNode,
setInitialProperties,
diffProperties,
updateProperties,
diffHydratedProperties,
diffHydratedText,
warnForUnmatchedText,
warnForDeletedHydratableElement,
warnForDeletedHydratableText,
warnForInsertedHydratedElement,
warnForInsertedHydratedText,
} = ReactDOMFiberComponent;
const {updatedAncestorInfo} = validateDOMNesting;
const {precacheFiberNode, updateFiberProps} = ReactDOMComponentTree;
let SUPPRESS_HYDRATION_WARNING;
let topLevelUpdateWarnings;
let warnOnInvalidCallback;
let didWarnAboutUnstableCreatePortal = false;
if (__DEV__) {
SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
if (
typeof Map !== 'function' ||
Map.prototype == null ||
typeof Map.prototype.forEach !== 'function' ||
typeof Set !== 'function' ||
Set.prototype == null ||
typeof Set.prototype.clear !== 'function' ||
typeof Set.prototype.forEach !== 'function'
) {
warning(
false,
'React depends on Map and Set built-in types. Make sure that you load a ' +
'polyfill in older browsers. https://fb.me/react-polyfills',
);
}
topLevelUpdateWarnings = (container: DOMContainer) => {
if (container._reactRootContainer && container.nodeType !== COMMENT_NODE) {
const hostInstance = DOMRenderer.findHostInstanceWithNoPortals(
container._reactRootContainer._internalRoot.current,
);
if (hostInstance) {
warning(
hostInstance.parentNode === container,
'render(...): It looks like the React-rendered content of this ' +
'container was removed without using React. This is not ' +
'supported and will cause errors. Instead, call ' +
'ReactDOM.unmountComponentAtNode to empty a container.',
);
}
}
const isRootRenderedBySomeReact = !!container._reactRootContainer;
const rootEl = getReactRootElementInContainer(container);
const hasNonRootReactChild = !!(
rootEl && ReactDOMComponentTree.getInstanceFromNode(rootEl)
);
warning(
!hasNonRootReactChild || isRootRenderedBySomeReact,
'render(...): Replacing React-rendered children with a new root ' +
'component. If you intended to update the children of this node, ' +
'you should instead have the existing children update their state ' +
'and render the new components instead of calling ReactDOM.render.',
);
warning(
container.nodeType !== ELEMENT_NODE ||
!((container: any): Element).tagName ||
((container: any): Element).tagName.toUpperCase() !== 'BODY',
'render(): Rendering components directly into document.body is ' +
'discouraged, since its children are often manipulated by third-party ' +
'scripts and browser extensions. This may lead to subtle ' +
'reconciliation issues. Try rendering into a container element created ' +
'for your app.',
);
};
warnOnInvalidCallback = function(callback: mixed, callerName: string) {
warning(
callback === null || typeof callback === 'function',
'%s(...): Expected the last optional `callback` argument to be a ' +
'function. Instead received: %s.',
callerName,
callback,
);
};
}
ReactControlledComponent.injection.injectFiberControlledHostComponent(
ReactDOMFiberComponent,
);
type DOMContainer =
| (Element & {
_reactRootContainer: ?Root,
})
| (Document & {
_reactRootContainer: ?Root,
});
type Container = Element | Document;
type Props = {
autoFocus?: boolean,
children?: mixed,
hidden?: boolean,
suppressHydrationWarning?: boolean,
};
type Instance = Element;
type TextInstance = Text;
type HostContextDev = {
namespace: string,
ancestorInfo: mixed,
};
type HostContextProd = string;
type HostContext = HostContextDev | HostContextProd;
let eventsEnabled: ?boolean = null;
let selectionInformation: ?mixed = null;
type Batch = FiberRootBatch & {
render(children: ReactNodeList): Work,
then(onComplete: () => mixed): void,
commit(): void,
// The ReactRoot constuctor is hoisted but the prototype methods are not. If
// we move ReactRoot to be above ReactBatch, the inverse error occurs.
// $FlowFixMe Hoisting issue.
_root: Root,
_hasChildren: boolean,
_children: ReactNodeList,
_callbacks: Array<() => mixed> | null,
_didComplete: boolean,
};
function ReactBatch(root: ReactRoot) {
const expirationTime = DOMRenderer.computeUniqueAsyncExpiration();
this._expirationTime = expirationTime;
this._root = root;
this._next = null;
this._callbacks = null;
this._didComplete = false;
this._hasChildren = false;
this._children = null;
this._defer = true;
}
ReactBatch.prototype.render = function(children: ReactNodeList) {
invariant(
this._defer,
'batch.render: Cannot render a batch that already committed.',
);
this._hasChildren = true;
this._children = children;
const internalRoot = this._root._internalRoot;
const expirationTime = this._expirationTime;
const work = new ReactWork();
DOMRenderer.updateContainerAtExpirationTime(
children,
internalRoot,
null,
expirationTime,
work._onCommit,
);
return work;
};
ReactBatch.prototype.then = function(onComplete: () => mixed) {
if (this._didComplete) {
onComplete();
return;
}
let callbacks = this._callbacks;
if (callbacks === null) {
callbacks = this._callbacks = [];
}
callbacks.push(onComplete);
};
ReactBatch.prototype.commit = function() {
const internalRoot = this._root._internalRoot;
let firstBatch = internalRoot.firstBatch;
invariant(
this._defer && firstBatch !== null,
'batch.commit: Cannot commit a batch multiple times.',
);
if (!this._hasChildren) {
// This batch is empty. Return.
this._next = null;
this._defer = false;
return;
}
let expirationTime = this._expirationTime;
// Ensure this is the first batch in the list.
if (firstBatch !== this) {
// This batch is not the earliest batch. We need to move it to the front.
// Update its expiration time to be the expiration time of the earliest
// batch, so that we can flush it without flushing the other batches.
if (this._hasChildren) {
expirationTime = this._expirationTime = firstBatch._expirationTime;
// Rendering this batch again ensures its children will be the final state
// when we flush (updates are processed in insertion order: last
// update wins).
// TODO: This forces a restart. Should we print a warning?
this.render(this._children);
}
// Remove the batch from the list.
let previous = null;
let batch = firstBatch;
while (batch !== this) {
previous = batch;
batch = batch._next;
}
invariant(
previous !== null,
'batch.commit: Cannot commit a batch multiple times.',
);
previous._next = batch._next;
// Add it to the front.
this._next = firstBatch;
firstBatch = internalRoot.firstBatch = this;
}
// Synchronously flush all the work up to this batch's expiration time.
this._defer = false;
DOMRenderer.flushRoot(internalRoot, expirationTime);
// Pop the batch from the list.
const next = this._next;
this._next = null;
firstBatch = internalRoot.firstBatch = next;
// Append the next earliest batch's children to the update queue.
if (firstBatch !== null && firstBatch._hasChildren) {
firstBatch.render(firstBatch._children);
}
};
ReactBatch.prototype._onComplete = function() {
if (this._didComplete) {
return;
}
this._didComplete = true;
const callbacks = this._callbacks;
if (callbacks === null) {
return;
}
// TODO: Error handling.
for (let i = 0; i < callbacks.length; i++) {
const callback = callbacks[i];
callback();
}
};
type Work = {
then(onCommit: () => mixed): void,
_onCommit: () => void,
_callbacks: Array<() => mixed> | null,
_didCommit: boolean,
};
function ReactWork() {
this._callbacks = null;
this._didCommit = false;
// TODO: Avoid need to bind by replacing callbacks in the update queue with
// list of Work objects.
this._onCommit = this._onCommit.bind(this);
}
ReactWork.prototype.then = function(onCommit: () => mixed): void {
if (this._didCommit) {
onCommit();
return;
}
let callbacks = this._callbacks;
if (callbacks === null) {
callbacks = this._callbacks = [];
}
callbacks.push(onCommit);
};
ReactWork.prototype._onCommit = function(): void {
if (this._didCommit) {
return;
}
this._didCommit = true;
const callbacks = this._callbacks;
if (callbacks === null) {
return;
}
// TODO: Error handling.
for (let i = 0; i < callbacks.length; i++) {
const callback = callbacks[i];
invariant(
typeof callback === 'function',
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: %s',
callback,
);
callback();
}
};
type Root = {
render(children: ReactNodeList, callback: ?() => mixed): Work,
unmount(callback: ?() => mixed): Work,
legacy_renderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
callback: ?() => mixed,
): Work,
createBatch(): Batch,
_internalRoot: FiberRoot,
};
function ReactRoot(container: Container, isAsync: boolean, hydrate: boolean) {
const root = DOMRenderer.createContainer(container, isAsync, hydrate);
this._internalRoot = root;
}
ReactRoot.prototype.render = function(
children: ReactNodeList,
callback: ?() => mixed,
): Work {
const root = this._internalRoot;
const work = new ReactWork();
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'render');
}
if (callback !== null) {
work.then(callback);
}
DOMRenderer.updateContainer(children, root, null, work._onCommit);
return work;
};
ReactRoot.prototype.unmount = function(callback: ?() => mixed): Work {
const root = this._internalRoot;
const work = new ReactWork();
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'render');
}
if (callback !== null) {
work.then(callback);
}
DOMRenderer.updateContainer(null, root, null, work._onCommit);
return work;
};
ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
callback: ?() => mixed,
): Work {
const root = this._internalRoot;
const work = new ReactWork();
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'render');
}
if (callback !== null) {
work.then(callback);
}
DOMRenderer.updateContainer(children, root, parentComponent, work._onCommit);
return work;
};
ReactRoot.prototype.createBatch = function(): Batch {
const batch = new ReactBatch(this);
const expirationTime = batch._expirationTime;
const internalRoot = this._internalRoot;
const firstBatch = internalRoot.firstBatch;
if (firstBatch === null) {
internalRoot.firstBatch = batch;
batch._next = null;
} else {
// Insert sorted by expiration time then insertion order
let insertAfter = null;
let insertBefore = firstBatch;
while (
insertBefore !== null &&
insertBefore._expirationTime <= expirationTime
) {
insertAfter = insertBefore;
insertBefore = insertBefore._next;
}
batch._next = insertBefore;
if (insertAfter !== null) {
insertAfter._next = batch;
}
}
return batch;
};
/**
* True if the supplied DOM node is a valid node element.
*
* @param {?DOMElement} node The candidate DOM node.
* @return {boolean} True if the DOM is a valid DOM node.
* @internal
*/
function isValidContainer(node) {
return !!(
node &&
(node.nodeType === ELEMENT_NODE ||
node.nodeType === DOCUMENT_NODE ||
node.nodeType === DOCUMENT_FRAGMENT_NODE ||
(node.nodeType === COMMENT_NODE &&
node.nodeValue === ' react-mount-point-unstable '))
);
}
function getReactRootElementInContainer(container: any) {
if (!container) {
return null;
}
if (container.nodeType === DOCUMENT_NODE) {
return container.documentElement;
} else {
return container.firstChild;
}
}
function shouldHydrateDueToLegacyHeuristic(container) {
const rootElement = getReactRootElementInContainer(container);
return !!(
rootElement &&
rootElement.nodeType === ELEMENT_NODE &&
rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME)
);
}
function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
switch (type) {
case 'button':
case 'input':
case 'select':
case 'textarea':
return !!props.autoFocus;
}
return false;
}
const DOMRenderer = ReactFiberReconciler({
getRootHostContext(rootContainerInstance: Container): HostContext {
let type;
let namespace;
const nodeType = rootContainerInstance.nodeType;
switch (nodeType) {
case DOCUMENT_NODE:
case DOCUMENT_FRAGMENT_NODE: {
type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment';
let root = (rootContainerInstance: any).documentElement;
namespace = root ? root.namespaceURI : getChildNamespace(null, '');
break;
}
default: {
const container: any =
nodeType === COMMENT_NODE
? rootContainerInstance.parentNode
: rootContainerInstance;
const ownNamespace = container.namespaceURI || null;
type = container.tagName;
namespace = getChildNamespace(ownNamespace, type);
break;
}
}
if (__DEV__) {
const validatedTag = type.toLowerCase();
const ancestorInfo = updatedAncestorInfo(null, validatedTag, null);
return {namespace, ancestorInfo};
}
return namespace;
},
getChildHostContext(
parentHostContext: HostContext,
type: string,
): HostContext {
if (__DEV__) {
const parentHostContextDev = ((parentHostContext: any): HostContextDev);
const namespace = getChildNamespace(parentHostContextDev.namespace, type);
const ancestorInfo = updatedAncestorInfo(
parentHostContextDev.ancestorInfo,
type,
null,
);
return {namespace, ancestorInfo};
}
const parentNamespace = ((parentHostContext: any): HostContextProd);
return getChildNamespace(parentNamespace, type);
},
getPublicInstance(instance) {
return instance;
},
prepareForCommit(): void {
eventsEnabled = ReactBrowserEventEmitter.isEnabled();
selectionInformation = ReactInputSelection.getSelectionInformation();
ReactBrowserEventEmitter.setEnabled(false);
},
resetAfterCommit(): void {
ReactInputSelection.restoreSelection(selectionInformation);
selectionInformation = null;
ReactBrowserEventEmitter.setEnabled(eventsEnabled);
eventsEnabled = null;
},
createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): Instance {
let parentNamespace: string;
if (__DEV__) {
// TODO: take namespace into account when validating.
const hostContextDev = ((hostContext: any): HostContextDev);
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
if (
typeof props.children === 'string' ||
typeof props.children === 'number'
) {
const string = '' + props.children;
const ownAncestorInfo = updatedAncestorInfo(
hostContextDev.ancestorInfo,
type,
null,
);
validateDOMNesting(null, string, ownAncestorInfo);
}
parentNamespace = hostContextDev.namespace;
} else {
parentNamespace = ((hostContext: any): HostContextProd);
}
const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
return domElement;
},
appendInitialChild(
parentInstance: Instance,
child: Instance | TextInstance,
): void {
parentInstance.appendChild(child);
},
finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
): boolean {
setInitialProperties(domElement, type, props, rootContainerInstance);
return shouldAutoFocusHostComponent(type, props);
},
prepareUpdate(
domElement: Instance,
type: string,
oldProps: Props,
newProps: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): null | Array<mixed> {
if (__DEV__) {
const hostContextDev = ((hostContext: any): HostContextDev);
if (
typeof newProps.children !== typeof oldProps.children &&
(typeof newProps.children === 'string' ||
typeof newProps.children === 'number')
) {
const string = '' + newProps.children;
const ownAncestorInfo = updatedAncestorInfo(
hostContextDev.ancestorInfo,
type,
null,
);
validateDOMNesting(null, string, ownAncestorInfo);
}
}
return diffProperties(
domElement,
type,
oldProps,
newProps,
rootContainerInstance,
);
},
shouldSetTextContent(type: string, props: Props): boolean {
return (
type === 'textarea' ||
typeof props.children === 'string' ||
typeof props.children === 'number' ||
(typeof props.dangerouslySetInnerHTML === 'object' &&
props.dangerouslySetInnerHTML !== null &&
typeof props.dangerouslySetInnerHTML.__html === 'string')
);
},
shouldDeprioritizeSubtree(type: string, props: Props): boolean {
return !!props.hidden;
},
createTextInstance(
text: string,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): TextInstance {
if (__DEV__) {
const hostContextDev = ((hostContext: any): HostContextDev);
validateDOMNesting(null, text, hostContextDev.ancestorInfo);
}
const textNode: TextInstance = createTextNode(text, rootContainerInstance);
precacheFiberNode(internalInstanceHandle, textNode);
return textNode;
},
now: ReactDOMFrameScheduling.now,
mutation: {
commitMount(
domElement: Instance,
type: string,
newProps: Props,
internalInstanceHandle: Object,
): void {
// Despite the naming that might imply otherwise, this method only
// fires if there is an `Update` effect scheduled during mounting.
// This happens if `finalizeInitialChildren` returns `true` (which it
// does to implement the `autoFocus` attribute on the client). But
// there are also other cases when this might happen (such as patching
// up text content during hydration mismatch). So we'll check this again.
if (shouldAutoFocusHostComponent(type, newProps)) {
((domElement: any):
| HTMLButtonElement
| HTMLInputElement
| HTMLSelectElement
| HTMLTextAreaElement).focus();
}
},
commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
// Update the props handle so that we know which props are the ones with
// with current event handlers.
updateFiberProps(domElement, newProps);
// Apply the diff to the DOM node.
updateProperties(domElement, updatePayload, type, oldProps, newProps);
},
resetTextContent(domElement: Instance): void {
setTextContent(domElement, '');
},
commitTextUpdate(
textInstance: TextInstance,
oldText: string,
newText: string,
): void {
textInstance.nodeValue = newText;
},
appendChild(
parentInstance: Instance,
child: Instance | TextInstance,
): void {
parentInstance.appendChild(child);
},
appendChildToContainer(
container: Container,
child: Instance | TextInstance,
): void {
if (container.nodeType === COMMENT_NODE) {
(container.parentNode: any).insertBefore(child, container);
} else {
container.appendChild(child);
}
},
insertBefore(
parentInstance: Instance,
child: Instance | TextInstance,
beforeChild: Instance | TextInstance,
): void {
parentInstance.insertBefore(child, beforeChild);
},
insertInContainerBefore(
container: Container,
child: Instance | TextInstance,
beforeChild: Instance | TextInstance,
): void {
if (container.nodeType === COMMENT_NODE) {
(container.parentNode: any).insertBefore(child, beforeChild);
} else {
container.insertBefore(child, beforeChild);
}
},
removeChild(
parentInstance: Instance,
child: Instance | TextInstance,
): void {
parentInstance.removeChild(child);
},
removeChildFromContainer(
container: Container,
child: Instance | TextInstance,
): void {
if (container.nodeType === COMMENT_NODE) {
(container.parentNode: any).removeChild(child);
} else {
container.removeChild(child);
}
},
},
hydration: {
canHydrateInstance(
instance: Instance | TextInstance,
type: string,
props: Props,
): null | Instance {
if (
instance.nodeType !== ELEMENT_NODE ||
type.toLowerCase() !== instance.nodeName.toLowerCase()
) {
return null;
}
// This has now been refined to an element node.
return ((instance: any): Instance);
},
canHydrateTextInstance(
instance: Instance | TextInstance,
text: string,
): null | TextInstance {
if (text === '' || instance.nodeType !== TEXT_NODE) {
// Empty strings are not parsed by HTML so there won't be a correct match here.
return null;
}
// This has now been refined to a text node.
return ((instance: any): TextInstance);
},
getNextHydratableSibling(
instance: Instance | TextInstance,
): null | Instance | TextInstance {
let node = instance.nextSibling;
// Skip non-hydratable nodes.
while (
node &&
node.nodeType !== ELEMENT_NODE &&
node.nodeType !== TEXT_NODE
) {
node = node.nextSibling;
}
return (node: any);
},
getFirstHydratableChild(
parentInstance: Container | Instance,
): null | Instance | TextInstance {
let next = parentInstance.firstChild;
// Skip non-hydratable nodes.
while (
next &&
next.nodeType !== ELEMENT_NODE &&
next.nodeType !== TEXT_NODE
) {
next = next.nextSibling;
}
return (next: any);
},
hydrateInstance(
instance: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): null | Array<mixed> {
precacheFiberNode(internalInstanceHandle, instance);
// TODO: Possibly defer this until the commit phase where all the events
// get attached.
updateFiberProps(instance, props);
let parentNamespace: string;
if (__DEV__) {
const hostContextDev = ((hostContext: any): HostContextDev);
parentNamespace = hostContextDev.namespace;
} else {
parentNamespace = ((hostContext: any): HostContextProd);
}
return diffHydratedProperties(
instance,
type,
props,
parentNamespace,
rootContainerInstance,
);
},
hydrateTextInstance(
textInstance: TextInstance,
text: string,
internalInstanceHandle: Object,
): boolean {
precacheFiberNode(internalInstanceHandle, textInstance);
return diffHydratedText(textInstance, text);
},
didNotMatchHydratedContainerTextInstance(
parentContainer: Container,
textInstance: TextInstance,
text: string,
) {
if (__DEV__) {
warnForUnmatchedText(textInstance, text);
}
},
didNotMatchHydratedTextInstance(
parentType: string,
parentProps: Props,
parentInstance: Instance,
textInstance: TextInstance,
text: string,
) {
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
warnForUnmatchedText(textInstance, text);
}
},
didNotHydrateContainerInstance(
parentContainer: Container,
instance: Instance | TextInstance,
) {
if (__DEV__) {
if (instance.nodeType === 1) {
warnForDeletedHydratableElement(parentContainer, (instance: any));
} else {
warnForDeletedHydratableText(parentContainer, (instance: any));
}
}
},
didNotHydrateInstance(
parentType: string,
parentProps: Props,
parentInstance: Instance,
instance: Instance | TextInstance,
) {
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
if (instance.nodeType === 1) {
warnForDeletedHydratableElement(parentInstance, (instance: any));
} else {
warnForDeletedHydratableText(parentInstance, (instance: any));
}
}
},
didNotFindHydratableContainerInstance(
parentContainer: Container,
type: string,
props: Props,
) {
if (__DEV__) {
warnForInsertedHydratedElement(parentContainer, type, props);
}
},
didNotFindHydratableContainerTextInstance(
parentContainer: Container,
text: string,
) {
if (__DEV__) {
warnForInsertedHydratedText(parentContainer, text);
}
},
didNotFindHydratableInstance(
parentType: string,
parentProps: Props,
parentInstance: Instance,
type: string,
props: Props,
) {
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
warnForInsertedHydratedElement(parentInstance, type, props);
}
},
didNotFindHydratableTextInstance(
parentType: string,
parentProps: Props,
parentInstance: Instance,
text: string,
) {
if (__DEV__ && parentProps[SUPPRESS_HYDRATION_WARNING] !== true) {
warnForInsertedHydratedText(parentInstance, text);
}
},
},
scheduleDeferredCallback: ReactDOMFrameScheduling.rIC,
cancelDeferredCallback: ReactDOMFrameScheduling.cIC,
});
ReactGenericBatching.injection.injectRenderer(DOMRenderer);
let warnedAboutHydrateAPI = false;
function legacyCreateRootFromDOMContainer(
container: DOMContainer,
forceHydrate: boolean,
): Root {
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// First clear any existing content.
if (!shouldHydrate) {
let warned = false;
let rootSibling;
while ((rootSibling = container.lastChild)) {
if (__DEV__) {
if (
!warned &&
rootSibling.nodeType === ELEMENT_NODE &&
(rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)
) {
warned = true;
warning(
false,
'render(): Target node has markup rendered by React, but there ' +
'are unrelated nodes as well. This is most commonly caused by ' +
'white-space inserted around server-rendered markup.',
);
}
}
container.removeChild(rootSibling);
}
}
if (__DEV__) {
if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) {
warnedAboutHydrateAPI = true;
lowPriorityWarning(
false,
'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
'will stop working in React v17. Replace the ReactDOM.render() call ' +
'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
);
}
}
// Legacy roots are not async by default.
const isAsync = false;
return new ReactRoot(container, isAsync, shouldHydrate);
}
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: DOMContainer,
forceHydrate: boolean,
callback: ?Function,
) {
// TODO: Ensure all entry points contain this check
invariant(
isValidContainer(container),
'Target container is not a DOM element.',
);
if (__DEV__) {
topLevelUpdateWarnings(container);
}
// TODO: Without `any` type, Flow says "Property cannot be accessed on any
// member of intersection type." Whyyyyyy.
let root: Root = (container._reactRootContainer: any);
if (!root) {
// Initial mount
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = DOMRenderer.getPublicRootInstance(root._internalRoot);
originalCallback.call(instance);
};
}
// Initial mount should not be batched.
DOMRenderer.unbatchedUpdates(() => {
if (parentComponent != null) {
root.legacy_renderSubtreeIntoContainer(
parentComponent,
children,
callback,
);
} else {
root.render(children, callback);
}
});
} else {
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = DOMRenderer.getPublicRootInstance(root._internalRoot);
originalCallback.call(instance);
};
}
// Update
if (parentComponent != null) {
root.legacy_renderSubtreeIntoContainer(
parentComponent,
children,
callback,
);
} else {
root.render(children, callback);
}
}
return DOMRenderer.getPublicRootInstance(root._internalRoot);
}
function createPortal(
children: ReactNodeList,
container: DOMContainer,
key: ?string = null,
) {
invariant(
isValidContainer(container),
'Target container is not a DOM element.',
);
// TODO: pass ReactDOM portal implementation as third argument
return ReactPortal.createPortal(children, container, null, key);
}
const ReactDOM: Object = {
createPortal,
findDOMNode(
componentOrElement: Element | ?React$Component<any, any>,
): null | Element | Text {
if (__DEV__) {
let owner = (ReactCurrentOwner.current: any);
if (owner !== null) {
const warnedAboutRefsInRender =
owner.stateNode._warnedAboutRefsInRender;
warning(
warnedAboutRefsInRender,
'%s is accessing findDOMNode inside its render(). ' +
'render() should be a pure function of props and state. It should ' +
'never access something that requires stale data from the previous ' +
'render, such as refs. Move this logic to componentDidMount and ' +
'componentDidUpdate instead.',
getComponentName(owner) || 'A component',
);
owner.stateNode._warnedAboutRefsInRender = true;
}
}
if (componentOrElement == null) {
return null;
}
if ((componentOrElement: any).nodeType === ELEMENT_NODE) {
return (componentOrElement: any);
}
const inst = ReactInstanceMap.get(componentOrElement);
if (inst) {
return DOMRenderer.findHostInstance(inst);
}
if (typeof componentOrElement.render === 'function') {
invariant(false, 'Unable to find node on an unmounted component.');
} else {
invariant(
false,
'Element appears to be neither ReactComponent nor DOMNode. Keys: %s',
Object.keys(componentOrElement),
);
}
},
hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {
// TODO: throw or warn if we couldn't hydrate?
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
true,
callback,
);
},
render(
element: React$Element<any>,
container: DOMContainer,
callback: ?Function,
) {
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
},
unstable_renderSubtreeIntoContainer(
parentComponent: React$Component<any, any>,
element: React$Element<any>,
containerNode: DOMContainer,
callback: ?Function,
) {
invariant(
parentComponent != null && ReactInstanceMap.has(parentComponent),
'parentComponent must be a valid React Component',
);
return legacyRenderSubtreeIntoContainer(
parentComponent,
element,
containerNode,
false,
callback,
);
},
unmountComponentAtNode(container: DOMContainer) {
invariant(
isValidContainer(container),
'unmountComponentAtNode(...): Target container is not a DOM element.',
);
if (container._reactRootContainer) {
if (__DEV__) {
const rootEl = getReactRootElementInContainer(container);
const renderedByDifferentReact =
rootEl && !ReactDOMComponentTree.getInstanceFromNode(rootEl);
warning(
!renderedByDifferentReact,
"unmountComponentAtNode(): The node you're attempting to unmount " +
'was rendered by another copy of React.',
);
}
// Unmount should not be batched.
DOMRenderer.unbatchedUpdates(() => {
legacyRenderSubtreeIntoContainer(null, null, container, false, () => {
container._reactRootContainer = null;
});
});
// If you call unmountComponentAtNode twice in quick succession, you'll
// get `true` twice. That's probably fine?
return true;
} else {
if (__DEV__) {
const rootEl = getReactRootElementInContainer(container);
const hasNonRootReactChild = !!(
rootEl && ReactDOMComponentTree.getInstanceFromNode(rootEl)
);
// Check if the container itself is a React root node.
const isContainerReactRoot =
container.nodeType === 1 &&
isValidContainer(container.parentNode) &&
!!container.parentNode._reactRootContainer;
warning(
!hasNonRootReactChild,
"unmountComponentAtNode(): The node you're attempting to unmount " +
'was rendered by React and is not a top-level container. %s',
isContainerReactRoot
? 'You may have accidentally passed in a React root node instead ' +
'of its container.'
: 'Instead, have the parent component update its state and ' +
'rerender in order to remove this component.',
);
}
return false;
}
},
// Temporary alias since we already shipped React 16 RC with it.
// TODO: remove in React 17.
unstable_createPortal(...args) {
if (!didWarnAboutUnstableCreatePortal) {
didWarnAboutUnstableCreatePortal = true;
lowPriorityWarning(
false,
'The ReactDOM.unstable_createPortal() alias has been deprecated, ' +
'and will be removed in React 17+. Update your code to use ' +
'ReactDOM.createPortal() instead. It has the exact same API, ' +
'but without the "unstable_" prefix.',
);
}
return createPortal(...args);
},
unstable_batchedUpdates: DOMRenderer.batchedUpdates,
unstable_deferredUpdates: DOMRenderer.deferredUpdates,
flushSync: DOMRenderer.flushSync,
unstable_flushControlled: DOMRenderer.flushControlled,
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
// For TapEventPlugin which is popular in open source
EventPluginHub,
// Used by test-utils
EventPluginRegistry,
EventPropagators,
ReactControlledComponent,
ReactDOMComponentTree,
ReactDOMEventListener,
},
};
type RootOptions = {
hydrate?: boolean,
};
if (enableCreateRoot) {
ReactDOM.createRoot = function createRoot(
container: DOMContainer,
options?: RootOptions,
): ReactRoot {
const hydrate = options != null && options.hydrate === true;
return new ReactRoot(container, true, hydrate);
};
}
const foundDevTools = DOMRenderer.injectIntoDevTools({
findFiberByHostInstance: ReactDOMComponentTree.getClosestInstanceFromNode,
bundleType: __DEV__ ? 1 : 0,
version: ReactVersion,
rendererPackageName: 'react-dom',
});
if (__DEV__) {
if (
!foundDevTools &&
ExecutionEnvironment.canUseDOM &&
window.top === window.self
) {
// If we're in Chrome or Firefox, provide a download link if not installed.
if (
(navigator.userAgent.indexOf('Chrome') > -1 &&
navigator.userAgent.indexOf('Edge') === -1) ||
navigator.userAgent.indexOf('Firefox') > -1
) {
const protocol = window.location.protocol;
// Don't warn in exotic cases like chrome-extension://.
if (/^(https?|file):$/.test(protocol)) {
console.info(
'%cDownload the React DevTools ' +
'for a better development experience: ' +
'https://fb.me/react-devtools' +
(protocol === 'file:'
? '\nYou might need to use a local HTTP server (instead of file://): ' +
'https://fb.me/react-devtools-faq'
: ''),
'font-weight:bold',
);
}
}
}
}
export default ReactDOM;