From c2ad4af4a0c863afd412e233d076958676b51efb Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 27 Mar 2024 00:51:20 -0400 Subject: [PATCH] Move ReactDOMLegacy implementation into RootFB Only the FB entry point has legacy mode now so we can move the remaining code in there. --- .../react-devtools-shell/src/app/index.js | 3 + .../src/e2e-regression/app-legacy.js | 1 + packages/react-dom/index.classic.fb.js | 6 +- packages/react-dom/index.js | 3 - packages/react-dom/index.stable.js | 3 - .../react-dom/src/ReactDOMSharedInternals.js | 7 +- .../__tests__/ReactComponentLifeCycle-test.js | 6 +- ...dDOMNode-test.js => findDOMNodeFB-test.js} | 36 +- packages/react-dom/src/client/ReactDOM.js | 37 +- .../react-dom/src/client/ReactDOMLegacy.js | 434 ------------------ .../react-dom/src/client/ReactDOMRootFB.js | 119 +++++ packages/react-dom/unstable_testing.js | 3 - packages/react-dom/unstable_testing.stable.js | 3 - .../src/__tests__/ReactJSXRuntime-test.js | 7 +- packages/shared/ReactFeatureFlags.js | 2 +- 15 files changed, 161 insertions(+), 509 deletions(-) rename packages/react-dom/src/__tests__/{findDOMNode-test.js => findDOMNodeFB-test.js} (84%) delete mode 100644 packages/react-dom/src/client/ReactDOMLegacy.js diff --git a/packages/react-devtools-shell/src/app/index.js b/packages/react-devtools-shell/src/app/index.js index 69769f80d25a6..f991d61363bd1 100644 --- a/packages/react-devtools-shell/src/app/index.js +++ b/packages/react-devtools-shell/src/app/index.js @@ -69,6 +69,7 @@ function mountStrictApp(App) { } function mountLegacyApp(App: () => React$Node) { + // $FlowFixMe[prop-missing]: These are removed in 19. const {render, unmountComponentAtNode} = require('react-dom'); function LegacyRender() { @@ -77,8 +78,10 @@ function mountLegacyApp(App: () => React$Node) { const container = createContainer(); + // $FlowFixMe[not-a-function]: These are removed in 19. render(createElement(LegacyRender), container); + // $FlowFixMe: These are removed in 19. unmountFunctions.push(() => unmountComponentAtNode(container)); } diff --git a/packages/react-devtools-shell/src/e2e-regression/app-legacy.js b/packages/react-devtools-shell/src/e2e-regression/app-legacy.js index e8bb10fda091b..19ad2f0f6d953 100644 --- a/packages/react-devtools-shell/src/e2e-regression/app-legacy.js +++ b/packages/react-devtools-shell/src/e2e-regression/app-legacy.js @@ -15,6 +15,7 @@ function mountApp(App: () => React$Node) { ((document.body: any): HTMLBodyElement).appendChild(container); + // $FlowFixMe[prop-missing]: These are removed in 19. ReactDOM.render(, container); } function mountTestApp() { diff --git a/packages/react-dom/index.classic.fb.js b/packages/react-dom/index.classic.fb.js index 5d4cb1e11f822..e765898d91cb8 100644 --- a/packages/react-dom/index.classic.fb.js +++ b/packages/react-dom/index.classic.fb.js @@ -20,11 +20,8 @@ Object.assign((Internals: any), { export { createPortal, - findDOMNode, flushSync, - unmountComponentAtNode, unstable_createEventHandle, - unstable_renderSubtreeIntoContainer, unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority. useFormStatus, useFormState, @@ -42,6 +39,9 @@ export { hydrateRoot, render, unstable_batchedUpdates, + findDOMNode, + unstable_renderSubtreeIntoContainer, + unmountComponentAtNode, } from './src/client/ReactDOMRootFB'; export {Internals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED}; diff --git a/packages/react-dom/index.js b/packages/react-dom/index.js index 30ccf0a2eac89..4357044d90818 100644 --- a/packages/react-dom/index.js +++ b/packages/react-dom/index.js @@ -15,11 +15,8 @@ export { createRoot, hydrateRoot, flushSync, - render, - unmountComponentAtNode, unstable_batchedUpdates, unstable_createEventHandle, - unstable_renderSubtreeIntoContainer, unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority. useFormStatus, useFormState, diff --git a/packages/react-dom/index.stable.js b/packages/react-dom/index.stable.js index a817a6b3d9af3..6da13efa643cd 100644 --- a/packages/react-dom/index.stable.js +++ b/packages/react-dom/index.stable.js @@ -13,10 +13,7 @@ export { createRoot, hydrateRoot, flushSync, - render, - unmountComponentAtNode, unstable_batchedUpdates, - unstable_renderSubtreeIntoContainer, useFormStatus, useFormState, prefetchDNS, diff --git a/packages/react-dom/src/ReactDOMSharedInternals.js b/packages/react-dom/src/ReactDOMSharedInternals.js index 6de3648103591..7bd080ce48240 100644 --- a/packages/react-dom/src/ReactDOMSharedInternals.js +++ b/packages/react-dom/src/ReactDOMSharedInternals.js @@ -7,7 +7,6 @@ * @flow */ -import type {FindDOMNodeType} from './client/ReactDOMLegacy.js'; import type {HostDispatcher} from './shared/ReactDOMTypes'; type InternalsType = { @@ -16,7 +15,11 @@ type InternalsType = { ReactDOMCurrentDispatcher: { current: HostDispatcher, }, - findDOMNode: null | FindDOMNodeType, + findDOMNode: + | null + | (( + componentOrElement: React$Component, + ) => null | Element | Text), }; function noop() {} diff --git a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js index f89e95bcf5dde..29e27cf35799e 100644 --- a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js +++ b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js @@ -14,7 +14,6 @@ let act; let React; let ReactDOM; let ReactDOMClient; -let findDOMNode; const clone = function (o) { return JSON.parse(JSON.stringify(o)); @@ -95,8 +94,6 @@ describe('ReactComponentLifeCycle', () => { React = require('react'); ReactDOM = require('react-dom'); - findDOMNode = - ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.findDOMNode; ReactDOMClient = require('react-dom/client'); }); @@ -376,6 +373,7 @@ describe('ReactComponentLifeCycle', () => { expect(instance.updater.isMounted(instance)).toBe(false); }); + // @gate www && !disableLegacyMode it('warns if legacy findDOMNode is used inside render', async () => { class Component extends React.Component { state = {isMounted: false}; @@ -384,7 +382,7 @@ describe('ReactComponentLifeCycle', () => { } render() { if (this.state.isMounted) { - expect(findDOMNode(this).tagName).toBe('DIV'); + expect(React.findDOMNode(this).tagName).toBe('DIV'); } return
; } diff --git a/packages/react-dom/src/__tests__/findDOMNode-test.js b/packages/react-dom/src/__tests__/findDOMNodeFB-test.js similarity index 84% rename from packages/react-dom/src/__tests__/findDOMNode-test.js rename to packages/react-dom/src/__tests__/findDOMNodeFB-test.js index 6dfcae82d4cfa..d89dd6cc3c9e2 100644 --- a/packages/react-dom/src/__tests__/findDOMNode-test.js +++ b/packages/react-dom/src/__tests__/findDOMNodeFB-test.js @@ -11,16 +11,15 @@ const React = require('react'); const ReactDOM = require('react-dom'); -const findDOMNode = - ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.findDOMNode; const StrictMode = React.StrictMode; describe('findDOMNode', () => { + // @gate www it('findDOMNode should return null if passed null', () => { - expect(findDOMNode(null)).toBe(null); + expect(ReactDOM.findDOMNode(null)).toBe(null); }); - // @gate !disableLegacyMode + // @gate www && !disableLegacyMode it('findDOMNode should find dom element', () => { class MyNode extends React.Component { render() { @@ -34,13 +33,13 @@ describe('findDOMNode', () => { const container = document.createElement('div'); const myNode = ReactDOM.render(, container); - const myDiv = findDOMNode(myNode); - const mySameDiv = findDOMNode(myDiv); + const myDiv = ReactDOM.findDOMNode(myNode); + const mySameDiv = ReactDOM.findDOMNode(myDiv); expect(myDiv.tagName).toBe('DIV'); expect(mySameDiv).toBe(myDiv); }); - // @gate !disableLegacyMode + // @gate www && !disableLegacyMode it('findDOMNode should find dom element after an update from null', () => { function Bar({flag}) { if (flag) { @@ -57,23 +56,24 @@ describe('findDOMNode', () => { const container = document.createElement('div'); const myNodeA = ReactDOM.render(, container); - const a = findDOMNode(myNodeA); + const a = ReactDOM.findDOMNode(myNodeA); expect(a).toBe(null); const myNodeB = ReactDOM.render(, container); expect(myNodeA === myNodeB).toBe(true); - const b = findDOMNode(myNodeB); + const b = ReactDOM.findDOMNode(myNodeB); expect(b.tagName).toBe('SPAN'); }); + // @gate www it('findDOMNode should reject random objects', () => { expect(function () { - findDOMNode({foo: 'bar'}); + ReactDOM.findDOMNode({foo: 'bar'}); }).toThrowError('Argument appears to not be a ReactComponent. Keys: foo'); }); - // @gate !disableLegacyMode + // @gate www && !disableLegacyMode it('findDOMNode should reject unmounted objects with render func', () => { class Foo extends React.Component { render() { @@ -85,16 +85,16 @@ describe('findDOMNode', () => { const inst = ReactDOM.render(, container); ReactDOM.unmountComponentAtNode(container); - expect(() => findDOMNode(inst)).toThrowError( + expect(() => ReactDOM.findDOMNode(inst)).toThrowError( 'Unable to find node on an unmounted component.', ); }); - // @gate !disableLegacyMode + // @gate www && !disableLegacyMode it('findDOMNode should not throw an error when called within a component that is not mounted', () => { class Bar extends React.Component { UNSAFE_componentWillMount() { - expect(findDOMNode(this)).toBeNull(); + expect(ReactDOM.findDOMNode(this)).toBeNull(); } render() { @@ -107,7 +107,7 @@ describe('findDOMNode', () => { }).not.toThrow(); }); - // @gate !disableLegacyMode + // @gate www && !disableLegacyMode it('findDOMNode should warn if used to find a host component inside StrictMode', () => { let parent = undefined; let child = undefined; @@ -129,7 +129,7 @@ describe('findDOMNode', () => { ); let match; - expect(() => (match = findDOMNode(parent))).toErrorDev([ + expect(() => (match = ReactDOM.findDOMNode(parent))).toErrorDev([ 'Warning: findDOMNode is deprecated in StrictMode. ' + 'findDOMNode was passed an instance of ContainsStrictModeChild which renders StrictMode children. ' + 'Instead, add a ref directly to the element you want to reference. ' + @@ -141,7 +141,7 @@ describe('findDOMNode', () => { expect(match).toBe(child); }); - // @gate !disableLegacyMode + // @gate www && !disableLegacyMode it('findDOMNode should warn if passed a component that is inside StrictMode', () => { let parent = undefined; let child = undefined; @@ -162,7 +162,7 @@ describe('findDOMNode', () => { ); let match; - expect(() => (match = findDOMNode(parent))).toErrorDev([ + expect(() => (match = ReactDOM.findDOMNode(parent))).toErrorDev([ 'Warning: findDOMNode is deprecated in StrictMode. ' + 'findDOMNode was passed an instance of IsInStrictMode which is inside StrictMode. ' + 'Instead, add a ref directly to the element you want to reference. ' + diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 99d2b62f2f845..e0cbe30a079f3 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -8,22 +8,12 @@ */ import type {ReactNodeList} from 'shared/ReactTypes'; -import type { - Container, - PublicInstance, -} from 'react-dom-bindings/src/client/ReactFiberConfigDOM'; import type { RootType, HydrateRootOptions, CreateRootOptions, } from './ReactDOMRoot'; -import { - findDOMNode, - render, - unstable_renderSubtreeIntoContainer, - unmountComponentAtNode, -} from './ReactDOMLegacy'; import { createRoot as createRootImpl, hydrateRoot as hydrateRootImpl, @@ -35,6 +25,7 @@ import { flushSync as flushSyncWithoutWarningIfAlreadyRendering, isAlreadyRendering, injectIntoDevTools, + findHostInstance, } from 'react-reconciler/src/ReactFiberReconciler'; import {runWithPriority} from 'react-reconciler/src/ReactEventPriorities'; import {createPortal as createPortalImpl} from 'react-reconciler/src/ReactPortal'; @@ -99,20 +90,6 @@ function createPortal( return createPortalImpl(children, container, null, key); } -function renderSubtreeIntoContainer( - parentComponent: React$Component, - element: React$Element, - containerNode: Container, - callback: ?Function, -): React$Component | PublicInstance | null { - return unstable_renderSubtreeIntoContainer( - parentComponent, - element, - containerNode, - callback, - ); -} - function createRoot( container: Element | Document | DocumentFragment, options?: CreateRootOptions, @@ -163,6 +140,12 @@ function flushSync(fn: (() => R) | void): R | void { return flushSyncWithoutWarningIfAlreadyRendering(fn); } +function findDOMNode( + componentOrElement: React$Component, +): null | Element | Text { + return findHostInstance(componentOrElement); +} + // Expose findDOMNode on internals Internals.findDOMNode = findDOMNode; @@ -178,15 +161,9 @@ export { unstable_batchedUpdates, flushSync, ReactVersion as version, - // Disabled behind disableLegacyReactDOMAPIs - findDOMNode, - render, - unmountComponentAtNode, // exposeConcurrentModeAPIs createRoot, hydrateRoot, - // Disabled behind disableUnstableRenderSubtreeIntoContainer - renderSubtreeIntoContainer as unstable_renderSubtreeIntoContainer, // enableCreateEventHandleAPI createEventHandle as unstable_createEventHandle, // TODO: Remove this once callers migrate to alternatives. diff --git a/packages/react-dom/src/client/ReactDOMLegacy.js b/packages/react-dom/src/client/ReactDOMLegacy.js deleted file mode 100644 index 46220501a5e1e..0000000000000 --- a/packages/react-dom/src/client/ReactDOMLegacy.js +++ /dev/null @@ -1,434 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and 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, - PublicInstance, -} from 'react-dom-bindings/src/client/ReactFiberConfigDOM'; -import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; -import type {ReactNodeList} from 'shared/ReactTypes'; - -import {disableLegacyMode} from 'shared/ReactFeatureFlags'; -import {clearContainer} from 'react-dom-bindings/src/client/ReactFiberConfigDOM'; -import { - getInstanceFromNode, - isContainerMarkedAsRoot, - markContainerAsRoot, - unmarkContainerAsRoot, -} from 'react-dom-bindings/src/client/ReactDOMComponentTree'; -import {listenToAllSupportedEvents} from 'react-dom-bindings/src/events/DOMPluginEventSystem'; -import {isValidContainerLegacy} from './ReactDOMRoot'; -import { - DOCUMENT_NODE, - ELEMENT_NODE, - COMMENT_NODE, -} from 'react-dom-bindings/src/client/HTMLNodeType'; - -import { - createContainer, - createHydrationContainer, - findHostInstanceWithNoPortals, - updateContainer, - flushSync, - getPublicRootInstance, - findHostInstance, - findHostInstanceWithWarning, - defaultOnUncaughtError, - defaultOnCaughtError, -} from 'react-reconciler/src/ReactFiberReconciler'; -import {LegacyRoot} from 'react-reconciler/src/ReactRootTags'; -import getComponentNameFromType from 'shared/getComponentNameFromType'; -import ReactSharedInternals from 'shared/ReactSharedInternals'; -import {has as hasInstance} from 'shared/ReactInstanceMap'; - -const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; - -let topLevelUpdateWarnings; - -if (__DEV__) { - topLevelUpdateWarnings = (container: Container) => { - if (container._reactRootContainer && container.nodeType !== COMMENT_NODE) { - const hostInstance = findHostInstanceWithNoPortals( - container._reactRootContainer.current, - ); - if (hostInstance) { - if (hostInstance.parentNode !== container) { - console.error( - 'It looks like the React-rendered content of this ' + - 'container was removed without using React. This is not ' + - 'supported and will cause errors. Instead, call ' + - 'ReactDOM.unmountComponentAtNode to empty a container.', - ); - } - } - } - - const isRootRenderedBySomeReact = !!container._reactRootContainer; - const rootEl = getReactRootElementInContainer(container); - const hasNonRootReactChild = !!(rootEl && getInstanceFromNode(rootEl)); - - if (hasNonRootReactChild && !isRootRenderedBySomeReact) { - console.error( - 'Replacing React-rendered children with a new root ' + - 'component. If you intended to update the children of this node, ' + - 'you should instead have the existing children update their state ' + - 'and render the new components instead of calling ReactDOM.render.', - ); - } - }; -} - -function getReactRootElementInContainer(container: any) { - if (!container) { - return null; - } - - if (container.nodeType === DOCUMENT_NODE) { - return container.documentElement; - } else { - return container.firstChild; - } -} - -function noopOnRecoverableError() { - // This isn't reachable because onRecoverableError isn't called in the - // legacy API. -} - -function legacyCreateRootFromDOMContainer( - container: Container, - initialChildren: ReactNodeList, - parentComponent: ?React$Component, - callback: ?Function, - isHydrationContainer: boolean, -): FiberRoot { - if (isHydrationContainer) { - if (typeof callback === 'function') { - const originalCallback = callback; - callback = function () { - const instance = getPublicRootInstance(root); - originalCallback.call(instance); - }; - } - - const root: FiberRoot = createHydrationContainer( - initialChildren, - callback, - container, - LegacyRoot, - null, // hydrationCallbacks - false, // isStrictMode - false, // concurrentUpdatesByDefaultOverride, - '', // identifierPrefix - defaultOnUncaughtError, - defaultOnCaughtError, - noopOnRecoverableError, - // TODO(luna) Support hydration later - null, - null, - ); - container._reactRootContainer = root; - markContainerAsRoot(root.current, container); - - const rootContainerElement = - container.nodeType === COMMENT_NODE ? container.parentNode : container; - // $FlowFixMe[incompatible-call] - listenToAllSupportedEvents(rootContainerElement); - - flushSync(); - return root; - } else { - // First clear any existing content. - clearContainer(container); - - if (typeof callback === 'function') { - const originalCallback = callback; - callback = function () { - const instance = getPublicRootInstance(root); - originalCallback.call(instance); - }; - } - - const root = createContainer( - container, - LegacyRoot, - null, // hydrationCallbacks - false, // isStrictMode - false, // concurrentUpdatesByDefaultOverride, - '', // identifierPrefix - defaultOnUncaughtError, - defaultOnCaughtError, - noopOnRecoverableError, - null, // transitionCallbacks - ); - container._reactRootContainer = root; - markContainerAsRoot(root.current, container); - - const rootContainerElement = - container.nodeType === COMMENT_NODE ? container.parentNode : container; - // $FlowFixMe[incompatible-call] - listenToAllSupportedEvents(rootContainerElement); - - // Initial mount should not be batched. - flushSync(() => { - updateContainer(initialChildren, root, parentComponent, callback); - }); - - return root; - } -} - -function warnOnInvalidCallback(callback: mixed): void { - if (__DEV__) { - if (callback !== null && typeof callback !== 'function') { - console.error( - 'Expected the last optional `callback` argument to be a ' + - 'function. Instead received: %s.', - callback, - ); - } - } -} - -function legacyRenderSubtreeIntoContainer( - parentComponent: ?React$Component, - children: ReactNodeList, - container: Container, - forceHydrate: boolean, - callback: ?Function, -): React$Component | PublicInstance | null { - if (__DEV__) { - topLevelUpdateWarnings(container); - warnOnInvalidCallback(callback === undefined ? null : callback); - } - - const maybeRoot = container._reactRootContainer; - let root: FiberRoot; - if (!maybeRoot) { - // Initial mount - root = legacyCreateRootFromDOMContainer( - container, - children, - parentComponent, - callback, - forceHydrate, - ); - } else { - root = maybeRoot; - if (typeof callback === 'function') { - const originalCallback = callback; - callback = function () { - const instance = getPublicRootInstance(root); - originalCallback.call(instance); - }; - } - // Update - updateContainer(children, root, parentComponent, callback); - } - return getPublicRootInstance(root); -} - -export type FindDOMNodeType = typeof findDOMNode; - -export function findDOMNode( - componentOrElement: Element | ?React$Component, -): null | Element | Text { - if (__DEV__) { - const owner = (ReactCurrentOwner.current: any); - if (owner !== null && owner.stateNode !== null) { - const warnedAboutRefsInRender = owner.stateNode._warnedAboutRefsInRender; - if (!warnedAboutRefsInRender) { - console.error( - '%s is accessing findDOMNode inside its render(). ' + - 'render() should be a pure function of props and state. It should ' + - 'never access something that requires stale data from the previous ' + - 'render, such as refs. Move this logic to componentDidMount and ' + - 'componentDidUpdate instead.', - getComponentNameFromType(owner.type) || 'A component', - ); - } - owner.stateNode._warnedAboutRefsInRender = true; - } - } - if (componentOrElement == null) { - return null; - } - if ((componentOrElement: any).nodeType === ELEMENT_NODE) { - return (componentOrElement: any); - } - if (__DEV__) { - return findHostInstanceWithWarning(componentOrElement, 'findDOMNode'); - } - return findHostInstance(componentOrElement); -} - -export function render( - element: React$Element, - container: Container, - callback: ?Function, -): React$Component | PublicInstance | null { - if (disableLegacyMode) { - if (__DEV__) { - console.error( - 'ReactDOM.render was removed in React 19. Use createRoot instead.', - ); - } - throw new Error('ReactDOM: Unsupported Legacy Mode API.'); - } - if (__DEV__) { - console.error( - 'ReactDOM.render has not been supported since React 18. Use createRoot ' + - 'instead. Until you switch to the new API, your app will behave as ' + - "if it's running React 17. Learn " + - 'more: https://react.dev/link/switch-to-createroot', - ); - } - - if (!isValidContainerLegacy(container)) { - throw new Error('Target container is not a DOM element.'); - } - - if (__DEV__) { - const isModernRoot = - isContainerMarkedAsRoot(container) && - container._reactRootContainer === undefined; - if (isModernRoot) { - console.error( - 'You are calling ReactDOM.render() on a container that was previously ' + - 'passed to ReactDOMClient.createRoot(). This is not supported. ' + - 'Did you mean to call root.render(element)?', - ); - } - } - return legacyRenderSubtreeIntoContainer( - null, - element, - container, - false, - callback, - ); -} - -export function unstable_renderSubtreeIntoContainer( - parentComponent: React$Component, - element: React$Element, - containerNode: Container, - callback: ?Function, -): React$Component | PublicInstance | null { - if (disableLegacyMode) { - if (__DEV__) { - console.error( - 'ReactDOM.unstable_renderSubtreeIntoContainer() was removed in React 19. Consider using a portal instead.', - ); - } - throw new Error('ReactDOM: Unsupported Legacy Mode API.'); - } - if (__DEV__) { - console.error( - 'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported ' + - 'since React 18. Consider using a portal instead. Until you switch to ' + - "the createRoot API, your app will behave as if it's running React " + - '17. Learn more: https://react.dev/link/switch-to-createroot', - ); - } - - if (!isValidContainerLegacy(containerNode)) { - throw new Error('Target container is not a DOM element.'); - } - - if (parentComponent == null || !hasInstance(parentComponent)) { - throw new Error('parentComponent must be a valid React Component'); - } - - return legacyRenderSubtreeIntoContainer( - parentComponent, - element, - containerNode, - false, - callback, - ); -} - -export function unmountComponentAtNode(container: Container): boolean { - if (disableLegacyMode) { - if (__DEV__) { - console.error( - 'unmountComponentAtNode was removed in React 19. Use root.unmount() instead.', - ); - } - throw new Error('ReactDOM: Unsupported Legacy Mode API.'); - } - if (!isValidContainerLegacy(container)) { - throw new Error('Target container is not a DOM element.'); - } - - if (__DEV__) { - const isModernRoot = - isContainerMarkedAsRoot(container) && - container._reactRootContainer === undefined; - if (isModernRoot) { - console.error( - 'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' + - 'passed to ReactDOMClient.createRoot(). This is not supported. Did you mean to call root.unmount()?', - ); - } - } - - if (container._reactRootContainer) { - if (__DEV__) { - const rootEl = getReactRootElementInContainer(container); - const renderedByDifferentReact = rootEl && !getInstanceFromNode(rootEl); - if (renderedByDifferentReact) { - console.error( - "unmountComponentAtNode(): The node you're attempting to unmount " + - 'was rendered by another copy of React.', - ); - } - } - - // Unmount should not be batched. - flushSync(() => { - legacyRenderSubtreeIntoContainer(null, null, container, false, () => { - // $FlowFixMe[incompatible-type] This should probably use `delete container._reactRootContainer` - container._reactRootContainer = null; - unmarkContainerAsRoot(container); - }); - }); - // If you call unmountComponentAtNode twice in quick succession, you'll - // get `true` twice. That's probably fine? - return true; - } else { - if (__DEV__) { - const rootEl = getReactRootElementInContainer(container); - const hasNonRootReactChild = !!(rootEl && getInstanceFromNode(rootEl)); - - // Check if the container itself is a React root node. - const isContainerReactRoot = - container.nodeType === ELEMENT_NODE && - isValidContainerLegacy(container.parentNode) && - // $FlowFixMe[prop-missing] - // $FlowFixMe[incompatible-use] - !!container.parentNode._reactRootContainer; - - if (hasNonRootReactChild) { - console.error( - "unmountComponentAtNode(): The node you're attempting to unmount " + - 'was rendered by React and is not a top-level container. %s', - isContainerReactRoot - ? 'You may have accidentally passed in a React root node instead ' + - 'of its container.' - : 'Instead, have the parent component update its state and ' + - 'rerender in order to remove this component.', - ); - } - } - - return false; - } -} diff --git a/packages/react-dom/src/client/ReactDOMRootFB.js b/packages/react-dom/src/client/ReactDOMRootFB.js index 138f781e6caa7..4bf5b43f6e51d 100644 --- a/packages/react-dom/src/client/ReactDOMRootFB.js +++ b/packages/react-dom/src/client/ReactDOMRootFB.js @@ -33,11 +33,13 @@ import { getInstanceFromNode, isContainerMarkedAsRoot, markContainerAsRoot, + unmarkContainerAsRoot, } from 'react-dom-bindings/src/client/ReactDOMComponentTree'; import {listenToAllSupportedEvents} from 'react-dom-bindings/src/events/DOMPluginEventSystem'; import {isValidContainerLegacy} from './ReactDOMRoot'; import { DOCUMENT_NODE, + ELEMENT_NODE, COMMENT_NODE, } from 'react-dom-bindings/src/client/HTMLNodeType'; @@ -49,12 +51,17 @@ import { updateContainer, flushSync, getPublicRootInstance, + findHostInstance, + findHostInstanceWithWarning, defaultOnUncaughtError, defaultOnCaughtError, } from 'react-reconciler/src/ReactFiberReconciler'; import {LegacyRoot} from 'react-reconciler/src/ReactRootTags'; +import getComponentNameFromType from 'shared/getComponentNameFromType'; import {has as hasInstance} from 'shared/ReactInstanceMap'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; + import assign from 'shared/assign'; // Provided by www @@ -146,6 +153,8 @@ export function hydrateRoot( ); } +const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; + let topLevelUpdateWarnings; if (__DEV__) { @@ -331,6 +340,38 @@ function legacyRenderSubtreeIntoContainer( return getPublicRootInstance(root); } +export function findDOMNode( + componentOrElement: Element | ?React$Component, +): null | Element | Text { + if (__DEV__) { + const owner = (ReactCurrentOwner.current: any); + if (owner !== null && owner.stateNode !== null) { + const warnedAboutRefsInRender = owner.stateNode._warnedAboutRefsInRender; + if (!warnedAboutRefsInRender) { + console.error( + '%s is accessing findDOMNode inside its render(). ' + + 'render() should be a pure function of props and state. It should ' + + 'never access something that requires stale data from the previous ' + + 'render, such as refs. Move this logic to componentDidMount and ' + + 'componentDidUpdate instead.', + getComponentNameFromType(owner.type) || 'A component', + ); + } + owner.stateNode._warnedAboutRefsInRender = true; + } + } + if (componentOrElement == null) { + return null; + } + if ((componentOrElement: any).nodeType === ELEMENT_NODE) { + return (componentOrElement: any); + } + if (__DEV__) { + return findHostInstanceWithWarning(componentOrElement, 'findDOMNode'); + } + return findHostInstance(componentOrElement); +} + export function render( element: React$Element, container: Container, @@ -418,4 +459,82 @@ export function unstable_renderSubtreeIntoContainer( ); } +export function unmountComponentAtNode(container: Container): boolean { + if (disableLegacyMode) { + if (__DEV__) { + console.error( + 'unmountComponentAtNode was removed in React 19. Use root.unmount() instead.', + ); + } + throw new Error('ReactDOM: Unsupported Legacy Mode API.'); + } + if (!isValidContainerLegacy(container)) { + throw new Error('Target container is not a DOM element.'); + } + + if (__DEV__) { + const isModernRoot = + isContainerMarkedAsRoot(container) && + container._reactRootContainer === undefined; + if (isModernRoot) { + console.error( + 'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' + + 'passed to ReactDOMClient.createRoot(). This is not supported. Did you mean to call root.unmount()?', + ); + } + } + + if (container._reactRootContainer) { + if (__DEV__) { + const rootEl = getReactRootElementInContainer(container); + const renderedByDifferentReact = rootEl && !getInstanceFromNode(rootEl); + if (renderedByDifferentReact) { + console.error( + "unmountComponentAtNode(): The node you're attempting to unmount " + + 'was rendered by another copy of React.', + ); + } + } + + // Unmount should not be batched. + flushSync(() => { + legacyRenderSubtreeIntoContainer(null, null, container, false, () => { + // $FlowFixMe[incompatible-type] This should probably use `delete container._reactRootContainer` + container._reactRootContainer = null; + unmarkContainerAsRoot(container); + }); + }); + // If you call unmountComponentAtNode twice in quick succession, you'll + // get `true` twice. That's probably fine? + return true; + } else { + if (__DEV__) { + const rootEl = getReactRootElementInContainer(container); + const hasNonRootReactChild = !!(rootEl && getInstanceFromNode(rootEl)); + + // Check if the container itself is a React root node. + const isContainerReactRoot = + container.nodeType === ELEMENT_NODE && + isValidContainerLegacy(container.parentNode) && + // $FlowFixMe[prop-missing] + // $FlowFixMe[incompatible-use] + !!container.parentNode._reactRootContainer; + + if (hasNonRootReactChild) { + console.error( + "unmountComponentAtNode(): The node you're attempting to unmount " + + 'was rendered by React and is not a top-level container. %s', + isContainerReactRoot + ? 'You may have accidentally passed in a React root node instead ' + + 'of its container.' + : 'Instead, have the parent component update its state and ' + + 'rerender in order to remove this component.', + ); + } + } + + return false; + } +} + export {batchedUpdates as unstable_batchedUpdates}; diff --git a/packages/react-dom/unstable_testing.js b/packages/react-dom/unstable_testing.js index 1e973748c7cb8..19cc1515cdffe 100644 --- a/packages/react-dom/unstable_testing.js +++ b/packages/react-dom/unstable_testing.js @@ -10,11 +10,8 @@ export { createPortal, flushSync, - render, - unmountComponentAtNode, unstable_batchedUpdates, unstable_createEventHandle, - unstable_renderSubtreeIntoContainer, unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority. useFormStatus, useFormState, diff --git a/packages/react-dom/unstable_testing.stable.js b/packages/react-dom/unstable_testing.stable.js index 0b47d34c0acb0..a0a460ce83a99 100644 --- a/packages/react-dom/unstable_testing.stable.js +++ b/packages/react-dom/unstable_testing.stable.js @@ -10,10 +10,7 @@ export { createPortal, flushSync, - render, - unmountComponentAtNode, unstable_batchedUpdates, - unstable_renderSubtreeIntoContainer, useFormStatus, useFormState, prefetchDNS, diff --git a/packages/react/src/__tests__/ReactJSXRuntime-test.js b/packages/react/src/__tests__/ReactJSXRuntime-test.js index db488e1684001..16acc60aef5b0 100644 --- a/packages/react/src/__tests__/ReactJSXRuntime-test.js +++ b/packages/react/src/__tests__/ReactJSXRuntime-test.js @@ -15,7 +15,6 @@ let ReactDOMClient; let JSXRuntime; let JSXDEVRuntime; let act; -let findDOMNode; // NOTE: Prefer to call the JSXRuntime directly in these tests so we can be // certain that we are testing the runtime behavior, as opposed to the Babel @@ -30,8 +29,6 @@ describe('ReactJSXRuntime', () => { ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); act = require('internal-test-utils').act; - findDOMNode = - ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.findDOMNode; }); it('allows static methods to be called using the type property', () => { @@ -133,9 +130,9 @@ describe('ReactJSXRuntime', () => { const outer = container.firstChild; if (__DEV__) { - expect(findDOMNode(outer).className).toBe('moo'); + expect(outer.className).toBe('moo'); } else { - expect(findDOMNode(outer).className).toBe('quack'); + expect(outer.className).toBe('quack'); } }); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 7113757b66676..e9f25eea1dc69 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -178,7 +178,7 @@ export const enableReactTestRendererWarning = __NEXT_MAJOR__; // Disables legacy mode // This allows us to land breaking changes to remove legacy mode APIs in experimental builds // before removing them in stable in the next Major -export const disableLegacyMode = __NEXT_MAJOR__; +export const disableLegacyMode = true; export const disableDOMTestUtils = __NEXT_MAJOR__;