| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | * |
| 7 | * @flow |
| 8 | */ |
| 9 | |
| 10 | import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes'; |
| 11 | import type { |
| 12 | FiberRoot, |
| 13 | TransitionTracingCallbacks, |
| 14 | } from 'react-reconciler/src/ReactInternalTypes'; |
| 15 | |
| 16 | import {isValidContainer} from 'react-dom-bindings/src/client/ReactDOMContainer'; |
| 17 | import {queueExplicitHydrationTarget} from 'react-dom-bindings/src/events/ReactDOMEventReplaying'; |
| 18 | import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; |
| 19 | import { |
| 20 | disableCommentsAsDOMContainers, |
| 21 | enableDefaultTransitionIndicator, |
| 22 | } from 'shared/ReactFeatureFlags'; |
| 23 | |
| 24 | export type RootType = { |
| 25 | render(children: ReactNodeList): void, |
| 26 | unmount(): void, |
| 27 | _internalRoot: FiberRoot | null, |
| 28 | }; |
| 29 | |
| 30 | export type CreateRootOptions = { |
| 31 | unstable_strictMode?: boolean, |
| 32 | unstable_transitionCallbacks?: TransitionTracingCallbacks, |
| 33 | identifierPrefix?: string, |
| 34 | onUncaughtError?: ( |
| 35 | error: mixed, |
| 36 | errorInfo: {+componentStack?: ?string}, |
| 37 | ) => void, |
| 38 | onCaughtError?: ( |
| 39 | error: mixed, |
| 40 | errorInfo: { |
| 41 | +componentStack?: ?string, |
| 42 | +errorBoundary?: ?component(...props: any), |
| 43 | }, |
| 44 | ) => void, |
| 45 | onRecoverableError?: ( |
| 46 | error: mixed, |
| 47 | errorInfo: {+componentStack?: ?string}, |
| 48 | ) => void, |
| 49 | onDefaultTransitionIndicator?: () => void | (() => void), |
| 50 | }; |
| 51 | |
| 52 | export type HydrateRootOptions = { |
| 53 | // Hydration options |
| 54 | onHydrated?: (hydrationBoundary: Comment) => void, |
| 55 | onDeleted?: (hydrationBoundary: Comment) => void, |
| 56 | // Options for all roots |
| 57 | unstable_strictMode?: boolean, |
| 58 | unstable_transitionCallbacks?: TransitionTracingCallbacks, |
| 59 | identifierPrefix?: string, |
| 60 | onUncaughtError?: ( |
| 61 | error: mixed, |
| 62 | errorInfo: {+componentStack?: ?string}, |
| 63 | ) => void, |
| 64 | onCaughtError?: ( |
| 65 | error: mixed, |
| 66 | errorInfo: { |
| 67 | +componentStack?: ?string, |
| 68 | +errorBoundary?: ?component(...props: any), |
| 69 | }, |
| 70 | ) => void, |
| 71 | onRecoverableError?: ( |
| 72 | error: mixed, |
| 73 | errorInfo: {+componentStack?: ?string}, |
| 74 | ) => void, |
| 75 | onDefaultTransitionIndicator?: () => void | (() => void), |
| 76 | formState?: ReactFormState<any, any> | null, |
| 77 | }; |
| 78 | |
| 79 | import { |
| 80 | isContainerMarkedAsRoot, |
| 81 | markContainerAsRoot, |
| 82 | unmarkContainerAsRoot, |
| 83 | } from 'react-dom-bindings/src/client/ReactDOMComponentTree'; |
| 84 | import {listenToAllSupportedEvents} from 'react-dom-bindings/src/events/DOMPluginEventSystem'; |
| 85 | import {COMMENT_NODE} from 'react-dom-bindings/src/client/HTMLNodeType'; |
| 86 | |
| 87 | import { |
| 88 | createContainer, |
| 89 | createHydrationContainer, |
| 90 | updateContainer, |
| 91 | updateContainerSync, |
| 92 | flushSyncWork, |
| 93 | isAlreadyRendering, |
| 94 | defaultOnUncaughtError, |
| 95 | defaultOnCaughtError, |
| 96 | defaultOnRecoverableError, |
| 97 | } from 'react-reconciler/src/ReactFiberReconciler'; |
| 98 | import {defaultOnDefaultTransitionIndicator} from './ReactDOMDefaultTransitionIndicator'; |
| 99 | import {ConcurrentRoot} from 'react-reconciler/src/ReactRootTags'; |
| 100 | |
| 101 | // $FlowFixMe[missing-this-annot] |
| 102 | function ReactDOMRoot(internalRoot: FiberRoot) { |
| 103 | this._internalRoot = internalRoot; |
| 104 | } |
| 105 | |
| 106 | // $FlowFixMe[prop-missing] found when upgrading Flow |
| 107 | ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = |
| 108 | // $FlowFixMe[missing-this-annot] |
| 109 | function (children: ReactNodeList): void { |
| 110 | const root = this._internalRoot; |
| 111 | if (root === null) { |
| 112 | throw new Error('Cannot update an unmounted root.'); |
| 113 | } |
| 114 | |
| 115 | if (__DEV__) { |
| 116 | // using a reference to `arguments` bails out of GCC optimizations which affect function arity |
| 117 | const args = arguments; |
| 118 | if (typeof args[1] === 'function') { |
| 119 | console.error( |
| 120 | 'does not support the second callback argument. ' + |
| 121 | 'To execute a side effect after rendering, declare it in a component body with useEffect().', |
| 122 | ); |
| 123 | } else if (isValidContainer(args[1])) { |
| 124 | console.error( |
| 125 | 'You passed a container to the second argument of root.render(...). ' + |
| 126 | "You don't need to pass it again since you already passed it to create the root.", |
| 127 | ); |
| 128 | } else if (typeof args[1] !== 'undefined') { |
| 129 | console.error( |
| 130 | 'You passed a second argument to root.render(...) but it only accepts ' + |
| 131 | 'one argument.', |
| 132 | ); |
| 133 | } |
| 134 | } |
| 135 | updateContainer(children, root, null, null); |
| 136 | }; |
| 137 | |
| 138 | // $FlowFixMe[prop-missing] found when upgrading Flow |
| 139 | ReactDOMHydrationRoot.prototype.unmount = ReactDOMRoot.prototype.unmount = |
| 140 | // $FlowFixMe[missing-this-annot] |
| 141 | function (): void { |
| 142 | if (__DEV__) { |
| 143 | // using a reference to `arguments` bails out of GCC optimizations which affect function arity |
| 144 | const args = arguments; |
| 145 | if (typeof args[0] === 'function') { |
| 146 | console.error( |
| 147 | 'does not support a callback argument. ' + |
| 148 | 'To execute a side effect after rendering, declare it in a component body with useEffect().', |
| 149 | ); |
| 150 | } |
| 151 | } |
| 152 | const root = this._internalRoot; |
| 153 | if (root !== null) { |
| 154 | this._internalRoot = null; |
| 155 | const container = root.containerInfo; |
| 156 | if (__DEV__) { |
| 157 | if (isAlreadyRendering()) { |
| 158 | console.error( |
| 159 | 'Attempted to synchronously unmount a root while React was already ' + |
| 160 | 'rendering. React cannot finish unmounting the root until the ' + |
| 161 | 'current render has completed, which may lead to a race condition.', |
| 162 | ); |
| 163 | } |
| 164 | } |
| 165 | updateContainerSync(null, root, null, null); |
| 166 | flushSyncWork(); |
| 167 | unmarkContainerAsRoot(container); |
| 168 | } |
| 169 | }; |
| 170 | |
| 171 | export function createRoot( |
| 172 | container: Element | Document | DocumentFragment, |
| 173 | options?: CreateRootOptions, |
| 174 | ): RootType { |
| 175 | if (!isValidContainer(container)) { |
| 176 | throw new Error('Target container is not a DOM element.'); |
| 177 | } |
| 178 | |
| 179 | warnIfReactDOMContainerInDEV(container); |
| 180 | |
| 181 | const concurrentUpdatesByDefaultOverride = false; |
| 182 | let isStrictMode = false; |
| 183 | let identifierPrefix = ''; |
| 184 | let onUncaughtError = defaultOnUncaughtError; |
| 185 | let onCaughtError = defaultOnCaughtError; |
| 186 | let onRecoverableError = defaultOnRecoverableError; |
| 187 | let onDefaultTransitionIndicator = defaultOnDefaultTransitionIndicator; |
| 188 | let transitionCallbacks = null; |
| 189 | |
| 190 | // $FlowFixMe[invalid-compare] |
| 191 | if (options !== null && options !== undefined) { |
| 192 | if (__DEV__) { |
| 193 | if ((options as any).hydrate) { |
| 194 | console.warn( |
| 195 | 'hydrate through createRoot is deprecated. Use ReactDOMClient.hydrateRoot(container, <App />) instead.', |
| 196 | ); |
| 197 | } else { |
| 198 | if ( |
| 199 | typeof options === 'object' && |
| 200 | // $FlowFixMe[invalid-compare] |
| 201 | options !== null && |
| 202 | (options as any).$$typeof === REACT_ELEMENT_TYPE |
| 203 | ) { |
| 204 | console.error( |
| 205 | 'You passed a JSX element to createRoot. You probably meant to ' + |
| 206 | 'call root.render instead. ' + |
| 207 | 'Example usage:\n\n' + |
| 208 | ' let root = createRoot(domContainer);\n' + |
| 209 | ' root.render(<App />);', |
| 210 | ); |
| 211 | } |
| 212 | } |
| 213 | } |
| 214 | if (options.unstable_strictMode === true) { |
| 215 | isStrictMode = true; |
| 216 | } |
| 217 | if (options.identifierPrefix !== undefined) { |
| 218 | identifierPrefix = options.identifierPrefix; |
| 219 | } |
| 220 | if (options.onUncaughtError !== undefined) { |
| 221 | onUncaughtError = options.onUncaughtError; |
| 222 | } |
| 223 | if (options.onCaughtError !== undefined) { |
| 224 | onCaughtError = options.onCaughtError; |
| 225 | } |
| 226 | if (options.onRecoverableError !== undefined) { |
| 227 | onRecoverableError = options.onRecoverableError; |
| 228 | } |
| 229 | if (enableDefaultTransitionIndicator) { |
| 230 | if (options.onDefaultTransitionIndicator !== undefined) { |
| 231 | onDefaultTransitionIndicator = options.onDefaultTransitionIndicator; |
| 232 | } |
| 233 | } |
| 234 | if (options.unstable_transitionCallbacks !== undefined) { |
| 235 | transitionCallbacks = options.unstable_transitionCallbacks; |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | const root = createContainer( |
| 240 | container, |
| 241 | ConcurrentRoot, |
| 242 | null, |
| 243 | isStrictMode, |
| 244 | concurrentUpdatesByDefaultOverride, |
| 245 | identifierPrefix, |
| 246 | onUncaughtError, |
| 247 | onCaughtError, |
| 248 | onRecoverableError, |
| 249 | onDefaultTransitionIndicator, |
| 250 | transitionCallbacks, |
| 251 | ); |
| 252 | markContainerAsRoot(root.current, container); |
| 253 | |
| 254 | const rootContainerElement: Document | Element | DocumentFragment = |
| 255 | !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE |
| 256 | ? (container.parentNode as any) |
| 257 | : container; |
| 258 | listenToAllSupportedEvents(rootContainerElement); |
| 259 | |
| 260 | // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions |
| 261 | return new ReactDOMRoot(root); |
| 262 | } |
| 263 | |
| 264 | // $FlowFixMe[missing-this-annot] |
| 265 | function ReactDOMHydrationRoot(internalRoot: FiberRoot) { |
| 266 | this._internalRoot = internalRoot; |
| 267 | } |
| 268 | function scheduleHydration(target: Node) { |
| 269 | if (target) { |
| 270 | queueExplicitHydrationTarget(target); |
| 271 | } |
| 272 | } |
| 273 | // $FlowFixMe[prop-missing] found when upgrading Flow |
| 274 | ReactDOMHydrationRoot.prototype.unstable_scheduleHydration = scheduleHydration; |
| 275 | |
| 276 | export function hydrateRoot( |
| 277 | container: Document | Element, |
| 278 | initialChildren: ReactNodeList, |
| 279 | options?: HydrateRootOptions, |
| 280 | ): RootType { |
| 281 | if (!isValidContainer(container)) { |
| 282 | throw new Error('Target container is not a DOM element.'); |
| 283 | } |
| 284 | |
| 285 | warnIfReactDOMContainerInDEV(container); |
| 286 | |
| 287 | if (__DEV__) { |
| 288 | if (initialChildren === undefined) { |
| 289 | console.error( |
| 290 | 'Must provide initial children as second argument to hydrateRoot. ' + |
| 291 | 'Example usage: hydrateRoot(domContainer, <App />)', |
| 292 | ); |
| 293 | } |
| 294 | } |
| 295 | |
| 296 | // For now we reuse the whole bag of options since they contain |
| 297 | // the hydration callbacks. |
| 298 | const hydrationCallbacks = options != null ? options : null; |
| 299 | |
| 300 | const concurrentUpdatesByDefaultOverride = false; |
| 301 | let isStrictMode = false; |
| 302 | let identifierPrefix = ''; |
| 303 | let onUncaughtError = defaultOnUncaughtError; |
| 304 | let onCaughtError = defaultOnCaughtError; |
| 305 | let onRecoverableError = defaultOnRecoverableError; |
| 306 | let onDefaultTransitionIndicator = defaultOnDefaultTransitionIndicator; |
| 307 | let transitionCallbacks = null; |
| 308 | let formState = null; |
| 309 | // $FlowFixMe[invalid-compare] |
| 310 | if (options !== null && options !== undefined) { |
| 311 | if (options.unstable_strictMode === true) { |
| 312 | isStrictMode = true; |
| 313 | } |
| 314 | if (options.identifierPrefix !== undefined) { |
| 315 | identifierPrefix = options.identifierPrefix; |
| 316 | } |
| 317 | if (options.onUncaughtError !== undefined) { |
| 318 | onUncaughtError = options.onUncaughtError; |
| 319 | } |
| 320 | if (options.onCaughtError !== undefined) { |
| 321 | onCaughtError = options.onCaughtError; |
| 322 | } |
| 323 | if (options.onRecoverableError !== undefined) { |
| 324 | onRecoverableError = options.onRecoverableError; |
| 325 | } |
| 326 | if (enableDefaultTransitionIndicator) { |
| 327 | if (options.onDefaultTransitionIndicator !== undefined) { |
| 328 | onDefaultTransitionIndicator = options.onDefaultTransitionIndicator; |
| 329 | } |
| 330 | } |
| 331 | if (options.unstable_transitionCallbacks !== undefined) { |
| 332 | transitionCallbacks = options.unstable_transitionCallbacks; |
| 333 | } |
| 334 | if (options.formState !== undefined) { |
| 335 | formState = options.formState; |
| 336 | } |
| 337 | } |
| 338 | |
| 339 | const root = createHydrationContainer( |
| 340 | initialChildren, |
| 341 | null, |
| 342 | container, |
| 343 | ConcurrentRoot, |
| 344 | hydrationCallbacks, |
| 345 | isStrictMode, |
| 346 | concurrentUpdatesByDefaultOverride, |
| 347 | identifierPrefix, |
| 348 | onUncaughtError, |
| 349 | onCaughtError, |
| 350 | onRecoverableError, |
| 351 | onDefaultTransitionIndicator, |
| 352 | transitionCallbacks, |
| 353 | formState, |
| 354 | ); |
| 355 | markContainerAsRoot(root.current, container); |
| 356 | // This can't be a comment node since hydration doesn't work on comment nodes anyway. |
| 357 | listenToAllSupportedEvents(container); |
| 358 | |
| 359 | // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions |
| 360 | return new ReactDOMHydrationRoot(root); |
| 361 | } |
| 362 | |
| 363 | function warnIfReactDOMContainerInDEV(container: any) { |
| 364 | if (__DEV__) { |
| 365 | if (isContainerMarkedAsRoot(container)) { |
| 366 | if (container._reactRootContainer) { |
| 367 | console.error( |
| 368 | 'You are calling ReactDOMClient.createRoot() on a container that was previously ' + |
| 369 | 'passed to ReactDOM.render(). This is not supported.', |
| 370 | ); |
| 371 | } else { |
| 372 | console.error( |
| 373 | 'You are calling ReactDOMClient.createRoot() on a container that ' + |
| 374 | 'has already been passed to createRoot() before. Instead, call ' + |
| 375 | 'root.render() on the existing root instead if you want to update it.', |
| 376 | ); |
| 377 | } |
| 378 | } |
| 379 | } |
| 380 | } |