diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 3c984695b3..b27de7a241 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -7,30 +7,28 @@ * @flow */ +import type {RootType} from './ReactDOMRoot'; 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 { + findDOMNode, + render, + hydrate, + unstable_renderSubtreeIntoContainer, + unmountComponentAtNode, +} from './ReactDOMLegacy'; +import {createRoot, createBlockingRoot, isValidContainer} from './ReactDOMRoot'; import { - findHostInstanceWithNoPortals, - createContainer, - updateContainer, batchedEventUpdates, batchedUpdates, - unbatchedUpdates, discreteUpdates, flushDiscreteUpdates, flushSync, flushControlled, injectIntoDevTools, - getPublicRootInstance, - findHostInstance, - findHostInstanceWithWarning, flushPassiveEffects, IsThisRendererActing, attemptSynchronousHydration, @@ -53,11 +51,7 @@ 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'; @@ -68,9 +62,6 @@ import { getNodeFromInstance, getFiberCurrentPropsFromNode, getClosestInstanceFromNode, - isContainerMarkedAsRoot, - markContainerAsRoot, - unmarkContainerAsRoot, } from './ReactDOMComponentTree'; import {restoreControlledState} from './ReactDOMComponent'; import {dispatchEvent} from '../events/ReactDOMEventListener'; @@ -79,26 +70,14 @@ import { 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__) { @@ -119,176 +98,9 @@ if (__DEV__) { '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, - }) - | (Document & { - _reactRootContainer: ?_ReactRoot, - }); - -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 ReactBlockingRoot( - 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 = ReactBlockingRoot.prototype.render = function( - children: ReactNodeList, - callback: ?() => mixed, -): void { - const root = this._internalRoot; - const cb = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(cb, 'render'); - } - updateContainer(children, root, null, cb); -}; - -ReactRoot.prototype.unmount = ReactBlockingRoot.prototype.unmount = function( - callback: ?() => mixed, -): void { - const root = this._internalRoot; - const cb = callback === undefined ? null : callback; - if (__DEV__) { - warnOnInvalidCallback(cb, 'render'); - } - const container = root.containerInfo; - updateContainer(null, root, null, () => { - unmarkContainerAsRoot(container); - if (cb !== null) { - cb(); - } - }); -}; - -/** - * 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, @@ -296,109 +108,13 @@ setBatchingImplementation( 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 ReactBlockingRoot( - container, - LegacyRoot, - shouldHydrate - ? { - hydrate: true, - } - : undefined, - ); -} - -function legacyRenderSubtreeIntoContainer( - parentComponent: ?React$Component, - 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); +export type DOMContainer = + | (Element & { + _reactRootContainer: ?RootType, + }) + | (Document & { + _reactRootContainer: ?RootType, }); - } 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, @@ -416,186 +132,12 @@ function createPortal( const ReactDOM: Object = { createPortal, - findDOMNode( - componentOrElement: Element | ?React$Component, - ): 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__) { - const isModernRoot = - isContainerMarkedAsRoot(container) && - container._reactRootContainer === undefined; - if (isModernRoot) { - warningWithoutStack( - false, - '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, - container: DOMContainer, - callback: ?Function, - ) { - invariant( - isValidContainer(container), - 'Target container is not a DOM element.', - ); - if (__DEV__) { - const isModernRoot = - isContainerMarkedAsRoot(container) && - container._reactRootContainer === undefined; - if (isModernRoot) { - warningWithoutStack( - false, - '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, - element: React$Element, - 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__) { - const isModernRoot = - isContainerMarkedAsRoot(container) && - container._reactRootContainer === undefined; - if (isModernRoot) { - warningWithoutStack( - false, - '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; - unmarkContainerAsRoot(container); - }); - }); - // 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; - } - }, + // Legacy + findDOMNode, + hydrate, + render, + unstable_renderSubtreeIntoContainer, + unmountComponentAtNode, // Temporary alias since we already shipped React 16 RC with it. // TODO: remove in React 17. @@ -638,59 +180,6 @@ const ReactDOM: Object = { }, }; -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 createBlockingRoot( - container: DOMContainer, - options?: RootOptions, -): _ReactRoot { - invariant( - isValidContainer(container), - 'createRoot(...): Target container is not a DOM element.', - ); - warnIfReactDOMContainerInDEV(container); - return new ReactBlockingRoot(container, BatchedRoot, options); -} - -function warnIfReactDOMContainerInDEV(container) { - if (__DEV__) { - if (isContainerMarkedAsRoot(container)) { - if (container._reactRootContainer) { - warningWithoutStack( - false, - 'You are calling ReactDOM.createRoot() on a container that was previously ' + - 'passed to ReactDOM.render(). This is not supported.', - ); - } else { - warningWithoutStack( - false, - 'You are calling ReactDOM.createRoot() on a container that ' + - 'has already been passed to createRoot() before. Instead, call ' + - 'root.render() on the existing root instead if you want to update it.', - ); - } - } - } -} - if (exposeConcurrentModeAPIs) { ReactDOM.createRoot = createRoot; ReactDOM.createBlockingRoot = createBlockingRoot; diff --git a/packages/react-dom/src/client/ReactDOMLegacy.js b/packages/react-dom/src/client/ReactDOMLegacy.js new file mode 100644 index 0000000000..e32ef37e45 --- /dev/null +++ b/packages/react-dom/src/client/ReactDOMLegacy.js @@ -0,0 +1,396 @@ +/** + * 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 {DOMContainer} from './ReactDOM'; +import type {RootType} from './ReactDOMRoot'; +import type {ReactNodeList} from 'shared/ReactTypes'; + +import { + getInstanceFromNode, + isContainerMarkedAsRoot, + unmarkContainerAsRoot, +} from './ReactDOMComponentTree'; +import { + createLegacyRoot, + isValidContainer, + warnOnInvalidCallback, +} from './ReactDOMRoot'; +import {ROOT_ATTRIBUTE_NAME} from '../shared/DOMProperty'; +import { + DOCUMENT_NODE, + ELEMENT_NODE, + COMMENT_NODE, +} from '../shared/HTMLNodeType'; + +import { + findHostInstanceWithNoPortals, + updateContainer, + unbatchedUpdates, + getPublicRootInstance, + findHostInstance, + findHostInstanceWithWarning, +} from 'react-reconciler/inline.dom'; +import getComponentName from 'shared/getComponentName'; +import invariant from 'shared/invariant'; +import lowPriorityWarningWithoutStack from 'shared/lowPriorityWarningWithoutStack'; +import warningWithoutStack from 'shared/warningWithoutStack'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import {has as hasInstance} from 'shared/ReactInstanceMap'; + +const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; + +let topLevelUpdateWarnings; +let warnedAboutHydrateAPI = false; + +if (__DEV__) { + 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.', + ); + }; +} + +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 legacyCreateRootFromDOMContainer( + container: DOMContainer, + forceHydrate: boolean, +): RootType { + 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.', + ); + } + } + + return createLegacyRoot( + container, + shouldHydrate + ? { + hydrate: true, + } + : undefined, + ); +} + +function legacyRenderSubtreeIntoContainer( + parentComponent: ?React$Component, + 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: RootType = (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); +} + +export function findDOMNode( + componentOrElement: Element | ?React$Component, +): 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); +} + +export function hydrate( + element: React$Node, + container: DOMContainer, + callback: ?Function, +) { + invariant( + isValidContainer(container), + 'Target container is not a DOM element.', + ); + if (__DEV__) { + const isModernRoot = + isContainerMarkedAsRoot(container) && + container._reactRootContainer === undefined; + if (isModernRoot) { + warningWithoutStack( + false, + '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, + ); +} + +export function render( + element: React$Element, + container: DOMContainer, + callback: ?Function, +) { + invariant( + isValidContainer(container), + 'Target container is not a DOM element.', + ); + if (__DEV__) { + const isModernRoot = + isContainerMarkedAsRoot(container) && + container._reactRootContainer === undefined; + if (isModernRoot) { + warningWithoutStack( + false, + '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, + ); +} + +export function unstable_renderSubtreeIntoContainer( + parentComponent: React$Component, + element: React$Element, + 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, + ); +} + +export function unmountComponentAtNode(container: DOMContainer) { + invariant( + isValidContainer(container), + 'unmountComponentAtNode(...): Target container is not a DOM element.', + ); + + if (__DEV__) { + const isModernRoot = + isContainerMarkedAsRoot(container) && + container._reactRootContainer === undefined; + if (isModernRoot) { + warningWithoutStack( + false, + '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; + unmarkContainerAsRoot(container); + }); + }); + // 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; + } +} diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js new file mode 100644 index 0000000000..0edc6a3672 --- /dev/null +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -0,0 +1,188 @@ +/** + * 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 {DOMContainer} from './ReactDOM'; +import type {RootTag} from 'shared/ReactRootTags'; +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} from 'react-reconciler/src/ReactFiberRoot'; + +export type RootType = { + render(children: ReactNodeList, callback: ?() => mixed): void, + unmount(callback: ?() => mixed): void, + + _internalRoot: FiberRoot, +}; + +export type RootOptions = { + hydrate?: boolean, + hydrationOptions?: { + onHydrated?: (suspenseNode: Comment) => void, + onDeleted?: (suspenseNode: Comment) => void, + }, +}; + +import { + isContainerMarkedAsRoot, + markContainerAsRoot, + unmarkContainerAsRoot, +} from './ReactDOMComponentTree'; +import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying'; +import { + ELEMENT_NODE, + COMMENT_NODE, + DOCUMENT_NODE, + DOCUMENT_FRAGMENT_NODE, +} from '../shared/HTMLNodeType'; + +import {createContainer, updateContainer} from 'react-reconciler/inline.dom'; +import invariant from 'shared/invariant'; +import warningWithoutStack from 'shared/warningWithoutStack'; +import {BlockingRoot, ConcurrentRoot, LegacyRoot} from 'shared/ReactRootTags'; + +function ReactDOMRoot(container: DOMContainer, options: void | RootOptions) { + this._internalRoot = createRootImpl(container, ConcurrentRoot, options); +} + +function ReactDOMBlockingRoot( + container: DOMContainer, + tag: RootTag, + options: void | RootOptions, +) { + this._internalRoot = createRootImpl(container, tag, options); +} + +ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render = function( + children: ReactNodeList, + callback: ?() => mixed, +): void { + const root = this._internalRoot; + const cb = callback === undefined ? null : callback; + if (__DEV__) { + warnOnInvalidCallback(cb, 'render'); + } + updateContainer(children, root, null, cb); +}; + +ReactDOMRoot.prototype.unmount = ReactDOMBlockingRoot.prototype.unmount = function( + callback: ?() => mixed, +): void { + const root = this._internalRoot; + const cb = callback === undefined ? null : callback; + if (__DEV__) { + warnOnInvalidCallback(cb, 'render'); + } + const container = root.containerInfo; + updateContainer(null, root, null, () => { + unmarkContainerAsRoot(container); + if (cb !== null) { + cb(); + } + }); +}; + +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; +} + +export function createRoot( + container: DOMContainer, + options?: RootOptions, +): RootType { + invariant( + isValidContainer(container), + 'createRoot(...): Target container is not a DOM element.', + ); + warnIfReactDOMContainerInDEV(container); + return new ReactDOMRoot(container, options); +} + +export function createBlockingRoot( + container: DOMContainer, + options?: RootOptions, +): RootType { + invariant( + isValidContainer(container), + 'createRoot(...): Target container is not a DOM element.', + ); + warnIfReactDOMContainerInDEV(container); + return new ReactDOMBlockingRoot(container, BlockingRoot, options); +} + +export function createLegacyRoot( + container: DOMContainer, + options?: RootOptions, +): RootType { + return new ReactDOMBlockingRoot(container, LegacyRoot, options); +} + +export function isValidContainer(node: mixed): boolean { + return !!( + node && + (node.nodeType === ELEMENT_NODE || + node.nodeType === DOCUMENT_NODE || + node.nodeType === DOCUMENT_FRAGMENT_NODE || + (node.nodeType === COMMENT_NODE && + (node: any).nodeValue === ' react-mount-point-unstable ')) + ); +} + +export function warnOnInvalidCallback( + callback: mixed, + callerName: string, +): void { + if (__DEV__) { + warningWithoutStack( + callback === null || typeof callback === 'function', + '%s(...): Expected the last optional `callback` argument to be a ' + + 'function. Instead received: %s.', + callerName, + callback, + ); + } +} + +function warnIfReactDOMContainerInDEV(container) { + if (__DEV__) { + if (isContainerMarkedAsRoot(container)) { + if (container._reactRootContainer) { + warningWithoutStack( + false, + 'You are calling ReactDOM.createRoot() on a container that was previously ' + + 'passed to ReactDOM.render(). This is not supported.', + ); + } else { + warningWithoutStack( + false, + 'You are calling ReactDOM.createRoot() on a container that ' + + 'has already been passed to createRoot() before. Instead, call ' + + 'root.render() on the existing root instead if you want to update it.', + ); + } + } + } +} diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 225d6e9695..24a368056c 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -26,7 +26,7 @@ import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; import enqueueTask from 'shared/enqueueTask'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import warningWithoutStack from 'shared/warningWithoutStack'; -import {ConcurrentRoot, BatchedRoot, LegacyRoot} from 'shared/ReactRootTags'; +import {ConcurrentRoot, BlockingRoot, LegacyRoot} from 'shared/ReactRootTags'; type Container = { rootID: string, @@ -952,7 +952,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { }; const fiberRoot = NoopRenderer.createContainer( container, - BatchedRoot, + BlockingRoot, false, null, ); diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 4264fb7088..3d83a94100 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -36,7 +36,7 @@ import { enableScopeAPI, } from 'shared/ReactFeatureFlags'; import {NoEffect, Placement} from 'shared/ReactSideEffectTags'; -import {ConcurrentRoot, BatchedRoot} from 'shared/ReactRootTags'; +import {ConcurrentRoot, BlockingRoot} from 'shared/ReactRootTags'; import { IndeterminateComponent, ClassComponent, @@ -574,7 +574,7 @@ export function createHostRootFiber(tag: RootTag): Fiber { let mode; if (tag === ConcurrentRoot) { mode = ConcurrentMode | BlockingMode | StrictMode; - } else if (tag === BatchedRoot) { + } else if (tag === BlockingRoot) { mode = BlockingMode | StrictMode; } else { mode = NoMode; diff --git a/packages/shared/ReactRootTags.js b/packages/shared/ReactRootTags.js index 44784b928d..409f4bd931 100644 --- a/packages/shared/ReactRootTags.js +++ b/packages/shared/ReactRootTags.js @@ -10,5 +10,5 @@ export type RootTag = 0 | 1 | 2; export const LegacyRoot = 0; -export const BatchedRoot = 1; +export const BlockingRoot = 1; export const ConcurrentRoot = 2;