/** * 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 {Container} from './ReactDOMHostConfig'; import type {MutableSource, ReactNodeList} from 'shared/ReactTypes'; import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; import {queueExplicitHydrationTarget} from '../events/ReactDOMEventReplaying'; export type RootType = { render(children: ReactNodeList): void, unmount(): void, _internalRoot: FiberRoot | null, ... }; export type CreateRootOptions = { // TODO: Remove these options. hydrate?: boolean, hydrationOptions?: { onHydrated?: (suspenseNode: Comment) => void, onDeleted?: (suspenseNode: Comment) => void, mutableSources?: Array>, ... }, // END OF TODO unstable_strictMode?: boolean, unstable_concurrentUpdatesByDefault?: boolean, identifierPrefix?: string, ... }; export type HydrateRootOptions = { // Hydration options hydratedSources?: Array>, onHydrated?: (suspenseNode: Comment) => void, onDeleted?: (suspenseNode: Comment) => void, // Options for all roots unstable_strictMode?: boolean, unstable_concurrentUpdatesByDefault?: boolean, identifierPrefix?: string, ... }; import { isContainerMarkedAsRoot, markContainerAsRoot, unmarkContainerAsRoot, } from './ReactDOMComponentTree'; import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem'; import { ELEMENT_NODE, COMMENT_NODE, DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE, } from '../shared/HTMLNodeType'; import { createContainer, updateContainer, findHostInstanceWithNoPortals, registerMutableSourceForHydration, flushSync, isAlreadyRendering, } from 'react-reconciler/src/ReactFiberReconciler'; import {ConcurrentRoot} from 'react-reconciler/src/ReactRootTags'; import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags'; function ReactDOMRoot(internalRoot: FiberRoot) { this._internalRoot = internalRoot; } ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function( children: ReactNodeList, ): void { const root = this._internalRoot; if (root === null) { throw new Error('Cannot update an unmounted root.'); } if (__DEV__) { if (typeof arguments[1] === 'function') { console.error( 'render(...): does not support the second callback argument. ' + 'To execute a side effect after rendering, declare it in a component body with useEffect().', ); } const container = root.containerInfo; if (container.nodeType !== COMMENT_NODE) { const hostInstance = findHostInstanceWithNoPortals(root.current); if (hostInstance) { if (hostInstance.parentNode !== container) { console.error( 'render(...): It looks like the React-rendered content of the ' + 'root container was removed without using React. This is not ' + 'supported and will cause errors. Instead, call ' + "root.unmount() to empty a root's container.", ); } } } } updateContainer(children, root, null, null); }; ReactDOMHydrationRoot.prototype.unmount = ReactDOMRoot.prototype.unmount = function(): void { if (__DEV__) { if (typeof arguments[0] === 'function') { console.error( 'unmount(...): does not support a callback argument. ' + 'To execute a side effect after rendering, declare it in a component body with useEffect().', ); } } const root = this._internalRoot; if (root !== null) { this._internalRoot = null; const container = root.containerInfo; if (__DEV__) { if (isAlreadyRendering()) { console.error( 'Attempted to synchronously unmount a root while React was already ' + 'rendering. React cannot finish unmounting the root until the ' + 'current render has completed, which may lead to a race condition.', ); } } flushSync(() => { updateContainer(null, root, null, null); }); unmarkContainerAsRoot(container); } }; export function createRoot( container: Container, options?: CreateRootOptions, ): RootType { if (!isValidContainerLegacy(container)) { throw new Error('createRoot(...): Target container is not a DOM element.'); } warnIfReactDOMContainerInDEV(container); // TODO: Delete these options const hydrate = options != null && options.hydrate === true; const hydrationCallbacks = (options != null && options.hydrationOptions) || null; const mutableSources = (options != null && options.hydrationOptions != null && options.hydrationOptions.mutableSources) || null; // END TODO let isStrictMode = false; let concurrentUpdatesByDefaultOverride = false; let identifierPrefix = ''; if (options !== null && options !== undefined) { if (options.unstable_strictMode === true) { isStrictMode = true; } if ( allowConcurrentByDefault && options.unstable_concurrentUpdatesByDefault === true ) { concurrentUpdatesByDefaultOverride = true; } if (options.identifierPrefix !== undefined) { identifierPrefix = options.identifierPrefix; } } const root = createContainer( container, ConcurrentRoot, hydrate, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, ); markContainerAsRoot(root.current, container); const rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container; listenToAllSupportedEvents(rootContainerElement); // TODO: Delete this path if (mutableSources) { for (let i = 0; i < mutableSources.length; i++) { const mutableSource = mutableSources[i]; registerMutableSourceForHydration(root, mutableSource); } } // END TODO return new ReactDOMRoot(root); } function ReactDOMHydrationRoot(internalRoot: FiberRoot) { this._internalRoot = internalRoot; } function scheduleHydration(target: Node) { if (target) { queueExplicitHydrationTarget(target); } } ReactDOMHydrationRoot.prototype.unstable_scheduleHydration = scheduleHydration; export function hydrateRoot( container: Container, initialChildren: ReactNodeList, options?: HydrateRootOptions, ): RootType { if (!isValidContainer(container)) { throw new Error('hydrateRoot(...): Target container is not a DOM element.'); } warnIfReactDOMContainerInDEV(container); // For now we reuse the whole bag of options since they contain // the hydration callbacks. const hydrationCallbacks = options != null ? options : null; // TODO: Delete this option const mutableSources = (options != null && options.hydratedSources) || null; let isStrictMode = false; let concurrentUpdatesByDefaultOverride = false; let identifierPrefix = ''; if (options !== null && options !== undefined) { if (options.unstable_strictMode === true) { isStrictMode = true; } if ( allowConcurrentByDefault && options.unstable_concurrentUpdatesByDefault === true ) { concurrentUpdatesByDefaultOverride = true; } if (options.identifierPrefix !== undefined) { identifierPrefix = options.identifierPrefix; } } const root = createContainer( container, ConcurrentRoot, true, // hydrate hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, ); markContainerAsRoot(root.current, container); // This can't be a comment node since hydration doesn't work on comment nodes anyway. listenToAllSupportedEvents(container); if (mutableSources) { for (let i = 0; i < mutableSources.length; i++) { const mutableSource = mutableSources[i]; registerMutableSourceForHydration(root, mutableSource); } } // Render the initial children updateContainer(initialChildren, root, null, null); return new ReactDOMHydrationRoot(root); } export function isValidContainer(node: any): boolean { return !!( node && (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE) ); } // TODO: Remove this function which also includes comment nodes. // We only use it in places that are currently more relaxed. export function isValidContainerLegacy(node: any): 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 ')) ); } function warnIfReactDOMContainerInDEV(container) { if (__DEV__) { if ( container.nodeType === ELEMENT_NODE && ((container: any): Element).tagName && ((container: any): Element).tagName.toUpperCase() === 'BODY' ) { console.error( 'createRoot(): Creating roots directly with 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 using a container element created ' + 'for your app.', ); } if (isContainerMarkedAsRoot(container)) { if (container._reactRootContainer) { console.error( 'You are calling ReactDOM.createRoot() on a container that was previously ' + 'passed to ReactDOM.render(). This is not supported.', ); } else { console.error( '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.', ); } } } }