mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
1355 lines
39 KiB
JavaScript
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;
|