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.
This commit is contained in:
Sebastian Markbåge 2021-06-15 16:37:53 -04:00 committed by GitHub
parent 9212d994ba
commit 7ec4c55971
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 141 additions and 70 deletions

View File

@ -23,6 +23,7 @@ export {
createPortal,
createRoot,
createRoot as unstable_createRoot, // TODO Remove once callsites use createRoot
hydrateRoot,
findDOMNode,
flushSync,
hydrate,

View File

@ -11,6 +11,7 @@ export {
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
createPortal,
createRoot,
hydrateRoot,
findDOMNode,
flushSync,
hydrate,

View File

@ -14,6 +14,7 @@ export {
createPortal,
createRoot,
createRoot as unstable_createRoot,
hydrateRoot,
findDOMNode,
flushSync,
hydrate,

View File

@ -12,6 +12,7 @@ export {
createPortal,
createRoot,
createRoot as unstable_createRoot, // TODO Remove once callsites use createRoot
hydrateRoot,
flushSync,
unstable_batchedUpdates,
unstable_createEventHandle,

View File

@ -11,6 +11,7 @@ export {
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
createPortal,
createRoot,
hydrateRoot,
findDOMNode,
flushSync,
hydrate,

View File

@ -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,
<div>
<span />
</div>,
@ -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.',
],

View File

@ -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

View File

@ -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, ...};

View File

@ -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) {

View File

@ -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<MutableSource<any>>,
...
},
// END OF TODO
unstable_strictMode?: boolean,
unstable_concurrentUpdatesByDefault?: boolean,
...
};
export type HydrateRootOptions = {
// Hydration options
hydratedSources?: Array<MutableSource<any>>,
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 ||

View File

@ -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."
}