diff --git a/packages/react-dom/index.classic.fb.js b/packages/react-dom/index.classic.fb.js index 234aa33066485..5f4f04e634615 100644 --- a/packages/react-dom/index.classic.fb.js +++ b/packages/react-dom/index.classic.fb.js @@ -23,6 +23,7 @@ export { createPortal, createRoot, createRoot as unstable_createRoot, // TODO Remove once callsites use createRoot + hydrateRoot, findDOMNode, flushSync, hydrate, diff --git a/packages/react-dom/index.experimental.js b/packages/react-dom/index.experimental.js index 5a1c9f191f702..5cb691740e714 100644 --- a/packages/react-dom/index.experimental.js +++ b/packages/react-dom/index.experimental.js @@ -11,6 +11,7 @@ export { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, createPortal, createRoot, + hydrateRoot, findDOMNode, flushSync, hydrate, diff --git a/packages/react-dom/index.js b/packages/react-dom/index.js index 6a824b2f70b14..7b89695091d54 100644 --- a/packages/react-dom/index.js +++ b/packages/react-dom/index.js @@ -14,6 +14,7 @@ export { createPortal, createRoot, createRoot as unstable_createRoot, + hydrateRoot, findDOMNode, flushSync, hydrate, diff --git a/packages/react-dom/index.modern.fb.js b/packages/react-dom/index.modern.fb.js index b53a8cf9add79..b12c249286b1b 100644 --- a/packages/react-dom/index.modern.fb.js +++ b/packages/react-dom/index.modern.fb.js @@ -12,6 +12,7 @@ export { createPortal, createRoot, createRoot as unstable_createRoot, // TODO Remove once callsites use createRoot + hydrateRoot, flushSync, unstable_batchedUpdates, unstable_createEventHandle, diff --git a/packages/react-dom/index.stable.js b/packages/react-dom/index.stable.js index 55d515a9fce2f..7d8b01be53e3d 100644 --- a/packages/react-dom/index.stable.js +++ b/packages/react-dom/index.stable.js @@ -11,6 +11,7 @@ export { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, createPortal, createRoot, + hydrateRoot, findDOMNode, flushSync, hydrate, diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 848e889a35dbe..7355e0be45a3c 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -17,7 +17,7 @@ import { unstable_renderSubtreeIntoContainer, unmountComponentAtNode, } from './ReactDOMLegacy'; -import {createRoot, isValidContainer} from './ReactDOMRoot'; +import {createRoot, hydrateRoot, isValidContainer} from './ReactDOMRoot'; import {createEventHandle} from './ReactDOMEventHandle'; import { @@ -182,6 +182,7 @@ export { unmountComponentAtNode, // exposeConcurrentModeAPIs createRoot, + hydrateRoot, flushControlled as unstable_flushControlled, scheduleHydration as unstable_scheduleHydration, // Disabled behind disableUnstableRenderSubtreeIntoContainer diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 908719d679891..cd8f913b9d513 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -105,8 +105,8 @@ export type EventTargetChildElement = { ... }; export type Container = - | (Element & {_reactRootContainer?: RootType, ...}) - | (Document & {_reactRootContainer?: RootType, ...}); + | (Element & {_reactRootContainer?: FiberRoot, ...}) + | (Document & {_reactRootContainer?: FiberRoot, ...}); export type Instance = Element; export type TextInstance = Text; export type SuspenseInstance = Comment & {_reactRetry?: () => void, ...}; diff --git a/packages/react-dom/src/client/ReactDOMLegacy.js b/packages/react-dom/src/client/ReactDOMLegacy.js index e85a5541b19e6..e6253b19dde6e 100644 --- a/packages/react-dom/src/client/ReactDOMLegacy.js +++ b/packages/react-dom/src/client/ReactDOMLegacy.js @@ -8,16 +8,17 @@ */ import type {Container} from './ReactDOMHostConfig'; -import type {RootType} from './ReactDOMRoot'; import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; import type {ReactNodeList} from 'shared/ReactTypes'; import { getInstanceFromNode, isContainerMarkedAsRoot, + markContainerAsRoot, unmarkContainerAsRoot, } from './ReactDOMComponentTree'; -import {createLegacyRoot, isValidContainer} from './ReactDOMRoot'; +import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem'; +import {isValidContainerLegacy} from './ReactDOMRoot'; import { DOCUMENT_NODE, ELEMENT_NODE, @@ -25,6 +26,7 @@ import { } from '../shared/HTMLNodeType'; import { + createContainer, findHostInstanceWithNoPortals, updateContainer, unbatchedUpdates, @@ -32,6 +34,7 @@ import { findHostInstance, findHostInstanceWithWarning, } from 'react-reconciler/src/ReactFiberReconciler'; +import {LegacyRoot} from 'react-reconciler/src/ReactRootTags'; import getComponentNameFromType from 'shared/getComponentNameFromType'; import invariant from 'shared/invariant'; import ReactSharedInternals from 'shared/ReactSharedInternals'; @@ -45,7 +48,7 @@ if (__DEV__) { topLevelUpdateWarnings = (container: Container) => { if (container._reactRootContainer && container.nodeType !== COMMENT_NODE) { const hostInstance = findHostInstanceWithNoPortals( - container._reactRootContainer._internalRoot.current, + container._reactRootContainer.current, ); if (hostInstance) { if (hostInstance.parentNode !== container) { @@ -103,7 +106,7 @@ function getReactRootElementInContainer(container: any) { function legacyCreateRootFromDOMContainer( container: Container, forceHydrate: boolean, -): RootType { +): FiberRoot { // First clear any existing content. if (!forceHydrate) { let rootSibling; @@ -112,14 +115,21 @@ function legacyCreateRootFromDOMContainer( } } - return createLegacyRoot( + const root = createContainer( container, - forceHydrate - ? { - hydrate: true, - } - : undefined, + LegacyRoot, + forceHydrate, + null, // hydrationCallbacks + false, // isStrictMode + false, // concurrentUpdatesByDefaultOverride, ); + markContainerAsRoot(root.current, container); + + const rootContainerElement = + container.nodeType === COMMENT_NODE ? container.parentNode : container; + listenToAllSupportedEvents(rootContainerElement); + + return root; } function warnOnInvalidCallback(callback: mixed, callerName: string): void { @@ -155,7 +165,7 @@ function legacyRenderSubtreeIntoContainer( container, forceHydrate, ); - fiberRoot = root._internalRoot; + fiberRoot = root; if (typeof callback === 'function') { const originalCallback = callback; callback = function() { @@ -168,7 +178,7 @@ function legacyRenderSubtreeIntoContainer( updateContainer(children, fiberRoot, parentComponent, callback); }); } else { - fiberRoot = root._internalRoot; + fiberRoot = root; if (typeof callback === 'function') { const originalCallback = callback; callback = function() { @@ -221,7 +231,7 @@ export function hydrate( ) { if (__DEV__) { console.error( - 'ReactDOM.hydrate is no longer supported in React 18. Use createRoot ' + + 'ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot ' + 'instead. Until you switch to the new API, your app will behave as ' + "if it's running React 17. Learn " + 'more: https://reactjs.org/link/switch-to-createroot', @@ -229,7 +239,7 @@ export function hydrate( } invariant( - isValidContainer(container), + isValidContainerLegacy(container), 'Target container is not a DOM element.', ); if (__DEV__) { @@ -240,7 +250,7 @@ export function hydrate( console.error( '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)?', + 'Did you mean to call hydrateRoot(container).render(element)?', ); } } @@ -269,7 +279,7 @@ export function render( } invariant( - isValidContainer(container), + isValidContainerLegacy(container), 'Target container is not a DOM element.', ); if (__DEV__) { @@ -300,7 +310,7 @@ export function unstable_renderSubtreeIntoContainer( callback: ?Function, ) { invariant( - isValidContainer(containerNode), + isValidContainerLegacy(containerNode), 'Target container is not a DOM element.', ); invariant( @@ -318,7 +328,7 @@ export function unstable_renderSubtreeIntoContainer( export function unmountComponentAtNode(container: Container) { invariant( - isValidContainer(container), + isValidContainerLegacy(container), 'unmountComponentAtNode(...): Target container is not a DOM element.', ); @@ -365,7 +375,7 @@ export function unmountComponentAtNode(container: Container) { // Check if the container itself is a React root node. const isContainerReactRoot = container.nodeType === ELEMENT_NODE && - isValidContainer(container.parentNode) && + isValidContainerLegacy(container.parentNode) && !!container.parentNode._reactRootContainer; if (hasNonRootReactChild) { diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index dc4c95a41ef30..616264f1502c0 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -19,7 +19,8 @@ export type RootType = { ... }; -export type RootOptions = { +export type CreateRootOptions = { + // TODO: Remove these options. hydrate?: boolean, hydrationOptions?: { onHydrated?: (suspenseNode: Comment) => void, @@ -27,6 +28,18 @@ export type RootOptions = { mutableSources?: Array>, ... }, + // END OF TODO + unstable_strictMode?: boolean, + unstable_concurrentUpdatesByDefault?: boolean, + ... +}; + +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, ... @@ -52,20 +65,14 @@ import { registerMutableSourceForHydration, } from 'react-reconciler/src/ReactFiberReconciler'; import invariant from 'shared/invariant'; -import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags'; +import {ConcurrentRoot} from 'react-reconciler/src/ReactRootTags'; import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags'; -function ReactDOMRoot(container: Container, options: void | RootOptions) { - this._internalRoot = createRootImpl(container, ConcurrentRoot, options); -} - -function ReactDOMLegacyRoot(container: Container, options: void | RootOptions) { - this._internalRoot = createRootImpl(container, LegacyRoot, options); +function ReactDOMRoot(internalRoot) { + this._internalRoot = internalRoot; } -ReactDOMRoot.prototype.render = ReactDOMLegacyRoot.prototype.render = function( - children: ReactNodeList, -): void { +ReactDOMRoot.prototype.render = function(children: ReactNodeList): void { const root = this._internalRoot; if (__DEV__) { if (typeof arguments[1] === 'function') { @@ -93,7 +100,7 @@ ReactDOMRoot.prototype.render = ReactDOMLegacyRoot.prototype.render = function( updateContainer(children, root, null, null); }; -ReactDOMRoot.prototype.unmount = ReactDOMLegacyRoot.prototype.unmount = function(): void { +ReactDOMRoot.prototype.unmount = function(): void { if (__DEV__) { if (typeof arguments[0] === 'function') { console.error( @@ -109,12 +116,17 @@ ReactDOMRoot.prototype.unmount = ReactDOMLegacyRoot.prototype.unmount = function }); }; -function createRootImpl( +export function createRoot( container: Container, - tag: RootTag, - options: void | RootOptions, -) { - // Tag is either LegacyRoot or Concurrent Root + options?: CreateRootOptions, +): RootType { + invariant( + isValidContainerLegacy(container), + '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; @@ -123,8 +135,9 @@ function createRootImpl( options.hydrationOptions != null && options.hydrationOptions.mutableSources) || null; - const isStrictMode = options != null && options.unstable_strictMode === true; + // END TODO + const isStrictMode = options != null && options.unstable_strictMode === true; let concurrentUpdatesByDefaultOverride = null; if (allowConcurrentByDefault) { concurrentUpdatesByDefaultOverride = @@ -135,7 +148,7 @@ function createRootImpl( const root = createContainer( container, - tag, + ConcurrentRoot, hydrate, hydrationCallbacks, isStrictMode, @@ -147,36 +160,80 @@ function createRootImpl( 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 root; + return new ReactDOMRoot(root); } -export function createRoot( +export function hydrateRoot( container: Container, - options?: RootOptions, + initialChildren: ReactNodeList, + options?: HydrateRootOptions, ): RootType { invariant( isValidContainer(container), - 'createRoot(...): Target container is not a DOM element.', + 'hydrateRoot(...): Target container is not a DOM element.', ); warnIfReactDOMContainerInDEV(container); - return new ReactDOMRoot(container, options); + + // For now we reuse the whole bag of options since they contain + // the hydration callbacks. + const hydrationCallbacks = options != null ? options : null; + const mutableSources = (options != null && options.hydratedSources) || null; + const isStrictMode = options != null && options.unstable_strictMode === true; + + let concurrentUpdatesByDefaultOverride = null; + if (allowConcurrentByDefault) { + concurrentUpdatesByDefaultOverride = + options != null && options.unstable_concurrentUpdatesByDefault != null + ? options.unstable_concurrentUpdatesByDefault + : null; + } + + const root = createContainer( + container, + ConcurrentRoot, + true, // hydrate + hydrationCallbacks, + isStrictMode, + concurrentUpdatesByDefaultOverride, + ); + 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 ReactDOMRoot(root); } -export function createLegacyRoot( - container: Container, - options?: RootOptions, -): RootType { - return new ReactDOMLegacyRoot(container, options); +export function isValidContainer(node: any): boolean { + return !!( + node && + (node.nodeType === ELEMENT_NODE || + node.nodeType === DOCUMENT_NODE || + node.nodeType === DOCUMENT_FRAGMENT_NODE) + ); } -export function isValidContainer(node: mixed): boolean { +// 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 ||