mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
4729ff6d1f
When an `identifierPrefix` option is given, React will add it to the beginning of ids generated by `useId`. The main use case is to avoid conflicts when there are multiple React roots on a single page. The server API already supported an `identifierPrefix` option. It's not only used by `useId`, but also for React-generated ids that are used to stitch together chunks of HTML, among other things. I added a corresponding option to the client. You must pass the same prefix option to both the server and client. Eventually we may make this automatic by sending the prefix from the server as part of the HTML stream.
332 lines
9.7 KiB
JavaScript
332 lines
9.7 KiB
JavaScript
/**
|
|
* 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<MutableSource<any>>,
|
|
...
|
|
},
|
|
// END OF TODO
|
|
unstable_strictMode?: boolean,
|
|
unstable_concurrentUpdatesByDefault?: boolean,
|
|
identifierPrefix?: string,
|
|
...
|
|
};
|
|
|
|
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,
|
|
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.',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|