Files
react/packages/react-dom/src/client/ReactDOM.js
T
Andrew ClarkandGitHub 7a3669200e Set up experimental builds (#17071)
* Don't bother including `unstable_` in error

The method names don't get stripped out of the production bundles
because they are passed as arguments to the error decoder.

Let's just always use the unprefixed APIs in the messages.

* Set up experimental builds

The experimental builds are packaged exactly like builds in the stable
release channel: same file structure, entry points, and npm package
names. The goal is to match what will eventually be released in stable
as closely as possible, but with additional features turned on.

Versioning and Releasing
------------------------

The experimental builds will be published to the same registry and
package names as the stable ones. However, they will be versioned using
a separate scheme. Instead of semver versions, experimental releases
will receive arbitrary version strings based on their content hashes.
The motivation is to thwart attempts to use a version range to match
against future experimental releases. The only way to install or depend
on an experimental release is to refer to the specific version number.

Building
--------

I did not use the existing feature flag infra to configure the
experimental builds. The reason is because feature flags are designed
to configure a single package. They're not designed to generate multiple
forks of the same package; for each set of feature flags, you must
create a separate package configuration.

Instead, I've added a new build dimension called the **release
channel**. By default, builds use the **stable** channel. There's
also an **experimental** release channel. We have the option to add more
in the future.

There are now two dimensions per artifact: build type (production,
development, or profiling), and release channel (stable or
experimental). These are separate dimensions because they are
combinatorial: there are stable and experimental production builds,
stable and experimental developmenet builds, and so on.

You can add something to an experimental build by gating on
`__EXPERIMENTAL__`, similar to how we use `__DEV__`. Anything inside
these branches will be excluded from the stable builds.
This gives us a low effort way to add experimental behavior in any
package without setting up feature flags or configuring a new package.
2019-10-14 10:46:42 -07:00

720 lines
22 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* 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';
import type {RootTag} from 'shared/ReactRootTags';
// TODO: This type is shared between the reconciler and ReactDOM, but will
// eventually be lifted out to the renderer.
import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';
import '../shared/checkReact';
import './ReactDOMClientInjection';
import {
findHostInstanceWithNoPortals,
createContainer,
updateContainer,
batchedEventUpdates,
batchedUpdates,
unbatchedUpdates,
discreteUpdates,
flushDiscreteUpdates,
flushSync,
flushControlled,
injectIntoDevTools,
getPublicRootInstance,
findHostInstance,
findHostInstanceWithWarning,
flushPassiveEffects,
IsThisRendererActing,
attemptSynchronousHydration,
attemptUserBlockingHydration,
attemptContinuousHydration,
attemptHydrationAtCurrentPriority,
} from 'react-reconciler/inline.dom';
import {createPortal as createPortalImpl} from 'shared/ReactPortal';
import {canUseDOM} from 'shared/ExecutionEnvironment';
import {setBatchingImplementation} from 'legacy-events/ReactGenericBatching';
import {
setRestoreImplementation,
enqueueStateRestore,
restoreStateIfNeeded,
} from 'legacy-events/ReactControlledComponent';
import {injection as EventPluginHubInjection} from 'legacy-events/EventPluginHub';
import {runEventsInBatch} from 'legacy-events/EventBatching';
import {eventNameDispatchConfigs} from 'legacy-events/EventPluginRegistry';
import {
accumulateTwoPhaseDispatches,
accumulateDirectDispatches,
} from 'legacy-events/EventPropagators';
import {LegacyRoot, ConcurrentRoot, BatchedRoot} from 'shared/ReactRootTags';
import {has as hasInstance} from 'shared/ReactInstanceMap';
import ReactVersion from 'shared/ReactVersion';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import getComponentName from 'shared/getComponentName';
import invariant from 'shared/invariant';
import lowPriorityWarningWithoutStack from 'shared/lowPriorityWarningWithoutStack';
import warningWithoutStack from 'shared/warningWithoutStack';
import {enableStableConcurrentModeAPIs} from 'shared/ReactFeatureFlags';
import {
getInstanceFromNode,
getNodeFromInstance,
getFiberCurrentPropsFromNode,
getClosestInstanceFromNode,
markContainerAsRoot,
} from './ReactDOMComponentTree';
import {restoreControlledState} from './ReactDOMComponent';
import {dispatchEvent} from '../events/ReactDOMEventListener';
import {
setAttemptSynchronousHydration,
setAttemptUserBlockingHydration,
setAttemptContinuousHydration,
setAttemptHydrationAtCurrentPriority,
eagerlyTrapReplayableEvents,
queueExplicitHydrationTarget,
} from '../events/ReactDOMEventReplaying';
import {
ELEMENT_NODE,
COMMENT_NODE,
DOCUMENT_NODE,
DOCUMENT_FRAGMENT_NODE,
} from '../shared/HTMLNodeType';
import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty';
setAttemptSynchronousHydration(attemptSynchronousHydration);
setAttemptUserBlockingHydration(attemptUserBlockingHydration);
setAttemptContinuousHydration(attemptContinuousHydration);
setAttemptHydrationAtCurrentPriority(attemptHydrationAtCurrentPriority);
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
let topLevelUpdateWarnings;
let warnOnInvalidCallback;
let didWarnAboutUnstableCreatePortal = false;
if (__DEV__) {
if (
typeof Map !== 'function' ||
// $FlowIssue Flow incorrectly thinks Map has no prototype
Map.prototype == null ||
typeof Map.prototype.forEach !== 'function' ||
typeof Set !== 'function' ||
// $FlowIssue Flow incorrectly thinks Set has no prototype
Set.prototype == null ||
typeof Set.prototype.clear !== 'function' ||
typeof Set.prototype.forEach !== 'function'
) {
warningWithoutStack(
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 = findHostInstanceWithNoPortals(
container._reactRootContainer._internalRoot.current,
);
if (hostInstance) {
warningWithoutStack(
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 && getInstanceFromNode(rootEl));
warningWithoutStack(
!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.',
);
warningWithoutStack(
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) {
warningWithoutStack(
callback === null || typeof callback === 'function',
'%s(...): Expected the last optional `callback` argument to be a ' +
'function. Instead received: %s.',
callerName,
callback,
);
};
}
setRestoreImplementation(restoreControlledState);
export type DOMContainer =
| (Element & {
_reactRootContainer: ?_ReactRoot,
_reactHasBeenPassedToCreateRootDEV: ?boolean,
})
| (Document & {
_reactRootContainer: ?_ReactRoot,
_reactHasBeenPassedToCreateRootDEV: ?boolean,
});
type _ReactRoot = {
render(children: ReactNodeList, callback: ?() => mixed): void,
unmount(callback: ?() => mixed): void,
_internalRoot: FiberRoot,
};
function createRootImpl(
container: DOMContainer,
tag: RootTag,
options: void | RootOptions,
) {
// Tag is either LegacyRoot or Concurrent Root
const hydrate = options != null && options.hydrate === true;
const hydrationCallbacks =
(options != null && options.hydrationOptions) || null;
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
markContainerAsRoot(root.current, container);
if (hydrate && tag !== LegacyRoot) {
const doc =
container.nodeType === DOCUMENT_NODE
? container
: container.ownerDocument;
eagerlyTrapReplayableEvents(doc);
}
return root;
}
function ReactSyncRoot(
container: DOMContainer,
tag: RootTag,
options: void | RootOptions,
) {
this._internalRoot = createRootImpl(container, tag, options);
}
function ReactRoot(container: DOMContainer, options: void | RootOptions) {
this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
}
ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function(
children: ReactNodeList,
callback: ?() => mixed,
): void {
const root = this._internalRoot;
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'render');
}
updateContainer(children, root, null, callback);
};
ReactRoot.prototype.unmount = ReactSyncRoot.prototype.unmount = function(
callback: ?() => mixed,
): void {
const root = this._internalRoot;
callback = callback === undefined ? null : callback;
if (__DEV__) {
warnOnInvalidCallback(callback, 'render');
}
updateContainer(null, root, null, callback);
};
/**
* 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)
);
}
setBatchingImplementation(
batchedUpdates,
discreteUpdates,
flushDiscreteUpdates,
batchedEventUpdates,
);
let warnedAboutHydrateAPI = false;
function legacyCreateRootFromDOMContainer(
container: DOMContainer,
forceHydrate: boolean,
): _ReactRoot {
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;
warningWithoutStack(
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;
lowPriorityWarningWithoutStack(
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 batched.
return new ReactSyncRoot(
container,
LegacyRoot,
shouldHydrate
? {
hydrate: true,
}
: undefined,
);
}
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: DOMContainer,
forceHydrate: boolean,
callback: ?Function,
) {
if (__DEV__) {
topLevelUpdateWarnings(container);
warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
}
// TODO: Without `any` type, Flow says "Property cannot be accessed on any
// member of intersection type." Whyyyyyy.
let root: _ReactRoot = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// Initial mount
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Initial mount should not be batched.
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Update
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
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 createPortalImpl(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 && owner.stateNode !== null) {
const warnedAboutRefsInRender =
owner.stateNode._warnedAboutRefsInRender;
warningWithoutStack(
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.type) || 'A component',
);
owner.stateNode._warnedAboutRefsInRender = true;
}
}
if (componentOrElement == null) {
return null;
}
if ((componentOrElement: any).nodeType === ELEMENT_NODE) {
return (componentOrElement: any);
}
if (__DEV__) {
return findHostInstanceWithWarning(componentOrElement, 'findDOMNode');
}
return findHostInstance(componentOrElement);
},
hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {
invariant(
isValidContainer(container),
'Target container is not a DOM element.',
);
if (__DEV__) {
warningWithoutStack(
!container._reactHasBeenPassedToCreateRootDEV,
'You are calling ReactDOM.hydrate() on a container that was previously ' +
'passed to ReactDOM.createRoot(). This is not supported. ' +
'Did you mean to call createRoot(container, {hydrate: true}).render(element)?',
);
}
// 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,
) {
invariant(
isValidContainer(container),
'Target container is not a DOM element.',
);
if (__DEV__) {
warningWithoutStack(
!container._reactHasBeenPassedToCreateRootDEV,
'You are calling ReactDOM.render() on a container that was previously ' +
'passed to ReactDOM.createRoot(). This is not supported. ' +
'Did you mean to call root.render(element)?',
);
}
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
},
unstable_renderSubtreeIntoContainer(
parentComponent: React$Component<any, any>,
element: React$Element<any>,
containerNode: DOMContainer,
callback: ?Function,
) {
invariant(
isValidContainer(containerNode),
'Target container is not a DOM element.',
);
invariant(
parentComponent != null && hasInstance(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 (__DEV__) {
warningWithoutStack(
!container._reactHasBeenPassedToCreateRootDEV,
'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
'passed to ReactDOM.createRoot(). This is not supported. Did you mean to call root.unmount()?',
);
}
if (container._reactRootContainer) {
if (__DEV__) {
const rootEl = getReactRootElementInContainer(container);
const renderedByDifferentReact = rootEl && !getInstanceFromNode(rootEl);
warningWithoutStack(
!renderedByDifferentReact,
"unmountComponentAtNode(): The node you're attempting to unmount " +
'was rendered by another copy of React.',
);
}
// Unmount should not be batched.
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 && getInstanceFromNode(rootEl));
// Check if the container itself is a React root node.
const isContainerReactRoot =
container.nodeType === ELEMENT_NODE &&
isValidContainer(container.parentNode) &&
!!container.parentNode._reactRootContainer;
warningWithoutStack(
!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;
lowPriorityWarningWithoutStack(
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: batchedUpdates,
// TODO remove this legacy method, unstable_discreteUpdates replaces it
unstable_interactiveUpdates: (fn, a, b, c) => {
flushDiscreteUpdates();
return discreteUpdates(fn, a, b, c);
},
unstable_discreteUpdates: discreteUpdates,
unstable_flushDiscreteUpdates: flushDiscreteUpdates,
flushSync: flushSync,
unstable_createRoot: createRoot,
unstable_createSyncRoot: createSyncRoot,
unstable_flushControlled: flushControlled,
unstable_scheduleHydration(target: Node) {
if (target) {
queueExplicitHydrationTarget(target);
}
},
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
// Keep in sync with ReactDOMUnstableNativeDependencies.js
// ReactTestUtils.js, and ReactTestUtilsAct.js. This is an array for better minification.
Events: [
getInstanceFromNode,
getNodeFromInstance,
getFiberCurrentPropsFromNode,
EventPluginHubInjection.injectEventPluginsByName,
eventNameDispatchConfigs,
accumulateTwoPhaseDispatches,
accumulateDirectDispatches,
enqueueStateRestore,
restoreStateIfNeeded,
dispatchEvent,
runEventsInBatch,
flushPassiveEffects,
IsThisRendererActing,
],
},
};
type RootOptions = {
hydrate?: boolean,
hydrationOptions?: {
onHydrated?: (suspenseNode: Comment) => void,
onDeleted?: (suspenseNode: Comment) => void,
},
};
function createRoot(
container: DOMContainer,
options?: RootOptions,
): _ReactRoot {
invariant(
isValidContainer(container),
'createRoot(...): Target container is not a DOM element.',
);
warnIfReactDOMContainerInDEV(container);
return new ReactRoot(container, options);
}
function createSyncRoot(
container: DOMContainer,
options?: RootOptions,
): _ReactRoot {
invariant(
isValidContainer(container),
'createRoot(...): Target container is not a DOM element.',
);
warnIfReactDOMContainerInDEV(container);
return new ReactSyncRoot(container, BatchedRoot, options);
}
function warnIfReactDOMContainerInDEV(container) {
if (__DEV__) {
warningWithoutStack(
!container._reactRootContainer,
'You are calling ReactDOM.createRoot() on a container that was previously ' +
'passed to ReactDOM.render(). This is not supported.',
);
container._reactHasBeenPassedToCreateRootDEV = true;
}
}
if (enableStableConcurrentModeAPIs) {
ReactDOM.createRoot = createRoot;
ReactDOM.createSyncRoot = createSyncRoot;
}
const foundDevTools = injectIntoDevTools({
findFiberByHostInstance: getClosestInstanceFromNode,
bundleType: __DEV__ ? 1 : 0,
version: ReactVersion,
rendererPackageName: 'react-dom',
});
if (__DEV__) {
if (!foundDevTools && 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;