From 7ec4c55971aad644616ca0b040f42410659fe802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 15 Jun 2021 16:37:53 -0400 Subject: [PATCH] createRoot(..., {hydrate:true}) -> hydrateRoot(...) (#21687) This adds a new top level API for hydrating a root. It takes the initial children as part of its constructor. These are unlike other render calls in that they have to represent what the server sent and they can't be batched with other updates. I also changed the options to move the hydrationOptions to the top level since now these options are all hydration options. I kept the createRoot one just temporarily to make it easier to codemod internally but I'm doing a follow up to delete. As part of this I un-dried a couple of paths. ReactDOMLegacy was intended to be built on top of the new API but it didn't actually use those root APIs because there are special paths. It also doesn't actually use most of the commmon paths since all the options are ignored. It also made it hard to add only warnings for legacy only or new only code paths. I also forked the create/hydrate paths because they're subtly different since now the options are different. The containers are also different because I now error for comment nodes during hydration which just doesn't work at all but eventually we'll error for all createRoot calls. After some iteration it might make sense to break out some common paths but for now it's easier to iterate on the duplicates. --- packages/react-dom/index.classic.fb.js | 1 + packages/react-dom/index.experimental.js | 1 + packages/react-dom/index.js | 1 + packages/react-dom/index.modern.fb.js | 1 + packages/react-dom/index.stable.js | 1 + .../src/__tests__/ReactDOMRoot-test.js | 7 +- packages/react-dom/src/client/ReactDOM.js | 3 +- .../src/client/ReactDOMHostConfig.js | 5 +- .../react-dom/src/client/ReactDOMLegacy.js | 48 +++--- packages/react-dom/src/client/ReactDOMRoot.js | 140 ++++++++++++------ scripts/error-codes/codes.json | 3 +- 11 files changed, 141 insertions(+), 70 deletions(-) diff --git a/packages/react-dom/index.classic.fb.js b/packages/react-dom/index.classic.fb.js index 234aa33066..5f4f04e634 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 5a1c9f191f..5cb691740e 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 6a824b2f70..7b89695091 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 b53a8cf9ad..b12c249286 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 55d515a9fc..7d8b01be53 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/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js index 9fe03cc2a9..6fb631fb4d 100644 --- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js @@ -96,11 +96,10 @@ describe('ReactDOMRoot', () => { ); Scheduler.unstable_flushAll(); - // Accepts `hydrate` option const container2 = document.createElement('div'); container2.innerHTML = markup; - const root2 = ReactDOM.createRoot(container2, {hydrate: true}); - root2.render( + ReactDOM.hydrateRoot( + container2,
, @@ -191,7 +190,7 @@ describe('ReactDOMRoot', () => { // We care about this warning: '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, element)?', // This is more of a symptom but restructuring the code to avoid it isn't worth it: 'Replacing React-rendered children with a new root component.', ], diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 848e889a35..7355e0be45 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 908719d679..bad70b6ba9 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -14,7 +14,6 @@ import type { IntersectionObserverOptions, ObserveVisibleRectsCallback, } from 'react-reconciler/src/ReactTestSelectors'; -import type {RootType} from './ReactDOMRoot'; import type {ReactScopeInstance} from 'shared/ReactTypes'; import { @@ -105,8 +104,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 e85a5541b1..a62a7fb444 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, 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 dc4c95a41e..ba062fc3f7 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -8,7 +8,6 @@ */ import type {Container} from './ReactDOMHostConfig'; -import type {RootTag} from 'react-reconciler/src/ReactRootTags'; import type {MutableSource, ReactNodeList} from 'shared/ReactTypes'; import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; @@ -19,7 +18,8 @@ export type RootType = { ... }; -export type RootOptions = { +export type CreateRootOptions = { + // TODO: Remove these options. hydrate?: boolean, hydrationOptions?: { onHydrated?: (suspenseNode: Comment) => void, @@ -27,6 +27,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 +64,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 ReactDOMRoot(internalRoot) { + this._internalRoot = internalRoot; } -function ReactDOMLegacyRoot(container: Container, options: void | RootOptions) { - this._internalRoot = createRootImpl(container, LegacyRoot, options); -} - -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 +99,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 +115,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,6 +134,58 @@ function createRootImpl( options.hydrationOptions != null && options.hydrationOptions.mutableSources) || null; + // END TODO + + 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, + hydrate, + hydrationCallbacks, + isStrictMode, + concurrentUpdatesByDefaultOverride, + ); + 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); +} + +export function hydrateRoot( + container: Container, + initialChildren: ReactNodeList, + options?: HydrateRootOptions, +): RootType { + invariant( + isValidContainer(container), + '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; + const mutableSources = (options != null && options.hydratedSources) || null; const isStrictMode = options != null && options.unstable_strictMode === true; let concurrentUpdatesByDefaultOverride = null; @@ -135,17 +198,15 @@ function createRootImpl( const root = createContainer( container, - tag, - hydrate, + ConcurrentRoot, + true, // hydrate hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, ); markContainerAsRoot(root.current, container); - - const rootContainerElement = - container.nodeType === COMMENT_NODE ? container.parentNode : container; - listenToAllSupportedEvents(rootContainerElement); + // 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++) { @@ -154,29 +215,24 @@ function createRootImpl( } } - return root; + // Render the initial children + updateContainer(initialChildren, root, null, null); + + return new ReactDOMRoot(root); } -export function createRoot( - container: Container, - options?: RootOptions, -): RootType { - invariant( - isValidContainer(container), - 'createRoot(...): Target container is not a DOM element.', +export function isValidContainer(node: any): boolean { + return !!( + node && + (node.nodeType === ELEMENT_NODE || + node.nodeType === DOCUMENT_NODE || + node.nodeType === DOCUMENT_FRAGMENT_NODE) ); - warnIfReactDOMContainerInDEV(container); - return new ReactDOMRoot(container, options); } -export function createLegacyRoot( - container: Container, - options?: RootOptions, -): RootType { - return new ReactDOMLegacyRoot(container, options); -} - -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 || diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 538f28756e..d80cd0776e 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -392,5 +392,6 @@ "401": "The stacks must reach the root at the same time. This is a bug in React.", "402": "The depth must equal at least at zero before reaching the root. This is a bug in React.", "403": "Tried to pop a Context at the root of the app. This is a bug in React.", - "404": "Invalid hook call. Hooks can only be called inside of the body of a function component." + "404": "Invalid hook call. Hooks can only be called inside of the body of a function component.", + "405": "hydrateRoot(...): Target container is not a DOM element." }