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:
parent
9212d994ba
commit
7ec4c55971
|
@ -23,6 +23,7 @@ export {
|
|||
createPortal,
|
||||
createRoot,
|
||||
createRoot as unstable_createRoot, // TODO Remove once callsites use createRoot
|
||||
hydrateRoot,
|
||||
findDOMNode,
|
||||
flushSync,
|
||||
hydrate,
|
||||
|
|
|
@ -11,6 +11,7 @@ export {
|
|||
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
||||
createPortal,
|
||||
createRoot,
|
||||
hydrateRoot,
|
||||
findDOMNode,
|
||||
flushSync,
|
||||
hydrate,
|
||||
|
|
|
@ -14,6 +14,7 @@ export {
|
|||
createPortal,
|
||||
createRoot,
|
||||
createRoot as unstable_createRoot,
|
||||
hydrateRoot,
|
||||
findDOMNode,
|
||||
flushSync,
|
||||
hydrate,
|
||||
|
|
|
@ -12,6 +12,7 @@ export {
|
|||
createPortal,
|
||||
createRoot,
|
||||
createRoot as unstable_createRoot, // TODO Remove once callsites use createRoot
|
||||
hydrateRoot,
|
||||
flushSync,
|
||||
unstable_batchedUpdates,
|
||||
unstable_createEventHandle,
|
||||
|
|
|
@ -11,6 +11,7 @@ export {
|
|||
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
||||
createPortal,
|
||||
createRoot,
|
||||
hydrateRoot,
|
||||
findDOMNode,
|
||||
flushSync,
|
||||
hydrate,
|
||||
|
|
|
@ -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.',
|
||||
],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, ...};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 ||
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue