main
js 511 lines 14.7 KB
Raw
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} from 'shared/ReactTypes';
11
12 import type {
13 RootType,
14 CreateRootOptions,
15 HydrateRootOptions,
16 } from './ReactDOMRoot';
17
18 import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
19
20 import type {
21 Container,
22 PublicInstance,
23 } from 'react-dom-bindings/src/client/ReactFiberConfigDOM';
24
25 import {
26 createRoot as createRootImpl,
27 hydrateRoot as hydrateRootImpl,
28 } from './ReactDOMRoot';
29
30 import {
31 disableLegacyMode,
32 disableCommentsAsDOMContainers,
33 } from 'shared/ReactFeatureFlags';
34 import {clearContainer} from 'react-dom-bindings/src/client/ReactFiberConfigDOM';
35 import {
36 getInstanceFromNode,
37 isContainerMarkedAsRoot,
38 markContainerAsRoot,
39 unmarkContainerAsRoot,
40 } from 'react-dom-bindings/src/client/ReactDOMComponentTree';
41 import {listenToAllSupportedEvents} from 'react-dom-bindings/src/events/DOMPluginEventSystem';
42 import {isValidContainer} from 'react-dom-bindings/src/client/ReactDOMContainer';
43 import {
44 DOCUMENT_NODE,
45 ELEMENT_NODE,
46 COMMENT_NODE,
47 } from 'react-dom-bindings/src/client/HTMLNodeType';
48
49 import {
50 batchedUpdates,
51 createContainer,
52 createHydrationContainer,
53 findHostInstanceWithNoPortals,
54 updateContainer,
55 updateContainerSync,
56 flushSyncWork,
57 getPublicRootInstance,
58 findHostInstance,
59 findHostInstanceWithWarning,
60 defaultOnUncaughtError,
61 defaultOnCaughtError,
62 } from 'react-reconciler/src/ReactFiberReconciler';
63 import {LegacyRoot} from 'react-reconciler/src/ReactRootTags';
64 import getComponentNameFromType from 'shared/getComponentNameFromType';
65
66 import {
67 current as currentOwner,
68 isRendering,
69 } from 'react-reconciler/src/ReactCurrentFiber';
70
71 import assign from 'shared/assign';
72
73 import noop from 'shared/noop';
74
75 // Provided by www
76 const ReactFiberErrorDialogWWW = require('ReactFiberErrorDialog');
77
78 if (typeof ReactFiberErrorDialogWWW.showErrorDialog !== 'function') {
79 throw new Error(
80 'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
81 );
82 }
83
84 function wwwOnUncaughtError(
85 error: mixed,
86 errorInfo: {+componentStack?: ?string},
87 ): void {
88 const componentStack =
89 errorInfo.componentStack != null ? errorInfo.componentStack : '';
90 const logError = ReactFiberErrorDialogWWW.showErrorDialog({
91 errorBoundary: null,
92 error,
93 componentStack,
94 });
95
96 // Allow injected showErrorDialog() to prevent default console.error logging.
97 // This enables renderers like ReactNative to better manage redbox behavior.
98 if (logError === false) {
99 return;
100 }
101
102 defaultOnUncaughtError(error, errorInfo);
103 }
104
105 function wwwOnCaughtError(
106 error: mixed,
107 errorInfo: {
108 +componentStack?: ?string,
109 +errorBoundary?: ?component(),
110 },
111 ): void {
112 const errorBoundary = errorInfo.errorBoundary;
113 const componentStack =
114 errorInfo.componentStack != null ? errorInfo.componentStack : '';
115 const logError = ReactFiberErrorDialogWWW.showErrorDialog({
116 errorBoundary,
117 error,
118 componentStack,
119 });
120
121 // Allow injected showErrorDialog() to prevent default console.error logging.
122 // This enables renderers like ReactNative to better manage redbox behavior.
123 if (logError === false) {
124 return;
125 }
126
127 defaultOnCaughtError(error, errorInfo);
128 }
129 const noopOnDefaultTransitionIndicator = noop;
130
131 export function createRoot(
132 container: Element | Document | DocumentFragment,
133 options?: CreateRootOptions,
134 ): RootType {
135 return createRootImpl(
136 container,
137 assign(
138 {
139 onUncaughtError: wwwOnUncaughtError,
140 onCaughtError: wwwOnCaughtError,
141 onDefaultTransitionIndicator: noopOnDefaultTransitionIndicator,
142 } as any,
143 options,
144 ),
145 );
146 }
147
148 export function hydrateRoot(
149 container: Document | Element,
150 initialChildren: ReactNodeList,
151 options?: HydrateRootOptions,
152 ): RootType {
153 return hydrateRootImpl(
154 container,
155 initialChildren,
156 assign(
157 {
158 onUncaughtError: wwwOnUncaughtError,
159 onCaughtError: wwwOnCaughtError,
160 onDefaultTransitionIndicator: noopOnDefaultTransitionIndicator,
161 } as any,
162 options,
163 ),
164 );
165 }
166
167 let topLevelUpdateWarnings;
168
169 if (__DEV__) {
170 topLevelUpdateWarnings = (container: Container) => {
171 if (container._reactRootContainer && container.nodeType !== COMMENT_NODE) {
172 const hostInstance = findHostInstanceWithNoPortals(
173 container._reactRootContainer.current,
174 );
175 if (hostInstance) {
176 if (hostInstance.parentNode !== container) {
177 console.error(
178 'It looks like the React-rendered content of this ' +
179 'container was removed without using React. This is not ' +
180 'supported and will cause errors. Instead, call ' +
181 'ReactDOM.unmountComponentAtNode to empty a container.',
182 );
183 }
184 }
185 }
186
187 const isRootRenderedBySomeReact = !!container._reactRootContainer;
188 const rootEl = getReactRootElementInContainer(container);
189 const hasNonRootReactChild = !!(rootEl && getInstanceFromNode(rootEl));
190
191 if (hasNonRootReactChild && !isRootRenderedBySomeReact) {
192 console.error(
193 'Replacing React-rendered children with a new root ' +
194 'component. If you intended to update the children of this node, ' +
195 'you should instead have the existing children update their state ' +
196 'and render the new components instead of calling ReactDOM.render.',
197 );
198 }
199 };
200 }
201
202 function getReactRootElementInContainer(container: any) {
203 if (!container) {
204 return null;
205 }
206
207 if (container.nodeType === DOCUMENT_NODE) {
208 return container.documentElement;
209 } else {
210 return container.firstChild;
211 }
212 }
213
214 // This isn't reachable because onRecoverableError isn't called in the
215 // legacy API.
216 const noopOnRecoverableError = noop;
217
218 function legacyCreateRootFromDOMContainer(
219 container: Container,
220 initialChildren: ReactNodeList,
221 parentComponent: ?component(...props: any),
222 callback: ?Function,
223 isHydrationContainer: boolean,
224 ): FiberRoot {
225 if (isHydrationContainer) {
226 if (typeof callback === 'function') {
227 const originalCallback = callback;
228 callback = function () {
229 const instance = getPublicRootInstance(root);
230 originalCallback.call(instance);
231 };
232 }
233
234 const root: FiberRoot = createHydrationContainer(
235 initialChildren,
236 callback,
237 container,
238 LegacyRoot,
239 null, // hydrationCallbacks
240 false, // isStrictMode
241 false, // concurrentUpdatesByDefaultOverride,
242 '', // identifierPrefix
243 wwwOnUncaughtError,
244 wwwOnCaughtError,
245 noopOnRecoverableError,
246 noopOnDefaultTransitionIndicator,
247 // TODO(luna) Support hydration later
248 null,
249 null,
250 );
251 container._reactRootContainer = root;
252 markContainerAsRoot(root.current, container);
253
254 const rootContainerElement =
255 !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE
256 ? container.parentNode
257 : container;
258 // $FlowFixMe[incompatible-type]
259 listenToAllSupportedEvents(rootContainerElement);
260
261 flushSyncWork();
262 return root;
263 } else {
264 // First clear any existing content.
265 clearContainer(container);
266
267 if (typeof callback === 'function') {
268 const originalCallback = callback;
269 callback = function () {
270 const instance = getPublicRootInstance(root);
271 originalCallback.call(instance);
272 };
273 }
274
275 const root = createContainer(
276 container,
277 LegacyRoot,
278 null, // hydrationCallbacks
279 false, // isStrictMode
280 false, // concurrentUpdatesByDefaultOverride,
281 '', // identifierPrefix
282 wwwOnUncaughtError,
283 wwwOnCaughtError,
284 noopOnRecoverableError,
285 noopOnDefaultTransitionIndicator,
286 null, // transitionCallbacks
287 );
288 container._reactRootContainer = root;
289 markContainerAsRoot(root.current, container);
290
291 const rootContainerElement =
292 !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE
293 ? container.parentNode
294 : container;
295 // $FlowFixMe[incompatible-type]
296 listenToAllSupportedEvents(rootContainerElement);
297
298 // Initial mount should not be batched.
299 updateContainerSync(initialChildren, root, parentComponent, callback);
300 flushSyncWork();
301
302 return root;
303 }
304 }
305
306 function warnOnInvalidCallback(callback: mixed): void {
307 if (__DEV__) {
308 if (callback !== null && typeof callback !== 'function') {
309 console.error(
310 'Expected the last optional `callback` argument to be a ' +
311 'function. Instead received: %s.',
312 callback,
313 );
314 }
315 }
316 }
317
318 function legacyRenderSubtreeIntoContainer(
319 parentComponent: ?component(...props: any),
320 children: ReactNodeList,
321 container: Container,
322 forceHydrate: boolean,
323 callback: ?Function,
324 ): component(...props: any) | PublicInstance | null {
325 if (__DEV__) {
326 topLevelUpdateWarnings(container);
327 warnOnInvalidCallback(callback === undefined ? null : callback);
328 }
329
330 const maybeRoot = container._reactRootContainer;
331 let root: FiberRoot;
332 if (!maybeRoot) {
333 // Initial mount
334 root = legacyCreateRootFromDOMContainer(
335 container,
336 children,
337 parentComponent,
338 callback,
339 forceHydrate,
340 );
341 } else {
342 root = maybeRoot;
343 if (typeof callback === 'function') {
344 const originalCallback = callback;
345 callback = function () {
346 const instance = getPublicRootInstance(root);
347 originalCallback.call(instance);
348 };
349 }
350 // Update
351 updateContainer(children, root, parentComponent, callback);
352 }
353 return getPublicRootInstance(root);
354 }
355
356 export function findDOMNode(
357 componentOrElement: Element | ?component(...props: any),
358 ): null | Element | Text {
359 if (__DEV__) {
360 const owner = currentOwner;
361 if (owner !== null && isRendering && owner.stateNode !== null) {
362 const warnedAboutRefsInRender = owner.stateNode._warnedAboutRefsInRender;
363 if (!warnedAboutRefsInRender) {
364 console.error(
365 '%s is accessing findDOMNode inside its render(). ' +
366 'render() should be a pure function of props and state. It should ' +
367 'never access something that requires stale data from the previous ' +
368 'render, such as refs. Move this logic to componentDidMount and ' +
369 'componentDidUpdate instead.',
370 getComponentNameFromType(owner.type) || 'A component',
371 );
372 }
373 owner.stateNode._warnedAboutRefsInRender = true;
374 }
375 }
376 if (componentOrElement == null) {
377 return null;
378 }
379 if ((componentOrElement as any).nodeType === ELEMENT_NODE) {
380 return componentOrElement as any;
381 }
382 if (__DEV__) {
383 return findHostInstanceWithWarning(componentOrElement, 'findDOMNode');
384 }
385 return findHostInstance(componentOrElement);
386 }
387
388 export function render(
389 element: React$Element<any>,
390 container: Container,
391 callback: ?Function,
392 ): component(...props: any) | PublicInstance | null {
393 if (disableLegacyMode) {
394 if (__DEV__) {
395 console.error(
396 'ReactDOM.render was removed in React 19. Use createRoot instead.',
397 );
398 }
399 throw new Error('ReactDOM: Unsupported Legacy Mode API.');
400 }
401 if (__DEV__) {
402 console.error(
403 'ReactDOM.render has not been supported since React 18. Use createRoot ' +
404 'instead. Until you switch to the new API, your app will behave as ' +
405 "if it's running React 17. Learn " +
406 'more: https://react.dev/link/switch-to-createroot',
407 );
408 }
409
410 if (!isValidContainer(container)) {
411 throw new Error('Target container is not a DOM element.');
412 }
413
414 if (__DEV__) {
415 const isModernRoot =
416 isContainerMarkedAsRoot(container) &&
417 container._reactRootContainer === undefined;
418 if (isModernRoot) {
419 console.error(
420 'You are calling ReactDOM.render() on a container that was previously ' +
421 'passed to ReactDOMClient.createRoot(). This is not supported. ' +
422 'Did you mean to call root.render(element)?',
423 );
424 }
425 }
426 return legacyRenderSubtreeIntoContainer(
427 null,
428 // $FlowFixMe[incompatible-type]
429 element,
430 container,
431 false,
432 callback,
433 );
434 }
435
436 export function unmountComponentAtNode(container: Container): boolean {
437 if (disableLegacyMode) {
438 if (__DEV__) {
439 console.error(
440 'unmountComponentAtNode was removed in React 19. Use root.unmount() instead.',
441 );
442 }
443 throw new Error('ReactDOM: Unsupported Legacy Mode API.');
444 }
445 if (!isValidContainer(container)) {
446 throw new Error('Target container is not a DOM element.');
447 }
448
449 if (__DEV__) {
450 const isModernRoot =
451 isContainerMarkedAsRoot(container) &&
452 container._reactRootContainer === undefined;
453 if (isModernRoot) {
454 console.error(
455 'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
456 'passed to ReactDOMClient.createRoot(). This is not supported. Did you mean to call root.unmount()?',
457 );
458 }
459 }
460
461 if (container._reactRootContainer) {
462 const root = container._reactRootContainer;
463
464 if (__DEV__) {
465 const rootEl = getReactRootElementInContainer(container);
466 const renderedByDifferentReact = rootEl && !getInstanceFromNode(rootEl);
467 if (renderedByDifferentReact) {
468 console.error(
469 "unmountComponentAtNode(): The node you're attempting to unmount " +
470 'was rendered by another copy of React.',
471 );
472 }
473 }
474
475 updateContainerSync(null, root, null, null);
476 flushSyncWork();
477 // $FlowFixMe[incompatible-type] This should probably use `delete container._reactRootContainer`
478 container._reactRootContainer = null;
479 unmarkContainerAsRoot(container);
480 return true;
481 } else {
482 if (__DEV__) {
483 const rootEl = getReactRootElementInContainer(container);
484 const hasNonRootReactChild = !!(rootEl && getInstanceFromNode(rootEl));
485
486 // Check if the container itself is a React root node.
487 const isContainerReactRoot =
488 container.nodeType === ELEMENT_NODE &&
489 isValidContainer(container.parentNode) &&
490 // $FlowFixMe[prop-missing]
491 // $FlowFixMe[incompatible-use]
492 !!container.parentNode._reactRootContainer;
493
494 if (hasNonRootReactChild) {
495 console.error(
496 "unmountComponentAtNode(): The node you're attempting to unmount " +
497 'was rendered by React and is not a top-level container. %s',
498 isContainerReactRoot
499 ? 'You may have accidentally passed in a React root node instead ' +
500 'of its container.'
501 : 'Instead, have the parent component update its state and ' +
502 'rerender in order to remove this component.',
503 );
504 }
505 }
506
507 return false;
508 }
509 }
510
511 export {batchedUpdates as unstable_batchedUpdates};