From 5113f272d640149aea53d0ace5aae8029c1466f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 2 Apr 2024 21:56:23 -0400 Subject: [PATCH] Move ReactDOMLegacy implementation into RootFB (#28656) Only the FB entry point has legacy mode now so we can move the remaining code in there. Also enable disableLegacyMode in modern www builds since it doesn't expose those entry points. Now dependent on #28709. --------- Co-authored-by: Josh Story --- .../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 +- .../ReactDeprecationWarnings-test.js | 6 +- .../src/__tests__/ReactTestUtils-test.js | 1 + ...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__/ReactScope-test.internal.js | 60 ++- .../__tests__/ReactSuspense-test.internal.js | 2 +- .../src/__tests__/ReactJSXRuntime-test.js | 9 +- packages/shared/ReactFeatureFlags.js | 2 +- .../forks/ReactFeatureFlags.test-renderer.js | 2 +- .../shared/forks/ReactFeatureFlags.www.js | 2 +- 21 files changed, 206 insertions(+), 539 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..4bea59a7b8e53 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(ReactDOM.findDOMNode(this).tagName).toBe('DIV'); } return
; } diff --git a/packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js b/packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js index 40b84fb2dac61..f2ec54f4ea018 100644 --- a/packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js +++ b/packages/react-dom/src/__tests__/ReactDeprecationWarnings-test.js @@ -103,9 +103,9 @@ describe('ReactDeprecationWarnings', () => { }); } } - expect(() => { - ReactNoop.renderLegacySyncRoot(); - }).toErrorDev([ + + ReactNoop.render(); + await expect(async () => await waitForAll([])).toErrorDev([ 'Component "Component" contains the string ref "refComponent". Support for string refs will be removed in a future major release.', ]); await waitForAll([]); diff --git a/packages/react-dom/src/__tests__/ReactTestUtils-test.js b/packages/react-dom/src/__tests__/ReactTestUtils-test.js index e3ed7c71a6883..80871a12d8e52 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtils-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtils-test.js @@ -586,6 +586,7 @@ describe('ReactTestUtils', () => { }); // @gate !disableDOMTestUtils + // @gate !disableLegacyMode it('should call setState callback with no arguments', async () => { let mockArgs; class Component extends React.Component { 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..76cb53beba5ec 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 && !disableLegacyMode 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 && !disableLegacyMode 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-reconciler/src/__tests__/ReactScope-test.internal.js b/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js index 4ad7bab5614bb..fd2982b81141f 100644 --- a/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactScope-test.internal.js @@ -393,7 +393,7 @@ describe('ReactScope', () => { }); // @gate www - it('DO_NOT_USE_queryAllNodes() works as intended', () => { + it('DO_NOT_USE_queryAllNodes() works as intended', async () => { const testScopeQuery = (type, props) => true; const TestScope = React.unstable_Scope; const scopeRef = React.createRef(); @@ -417,20 +417,25 @@ describe('ReactScope', () => { ); } - const renderer = ReactTestRenderer.create(, { - createNodeMock: element => { - return element; - }, - }); + let renderer; + await act( + () => + (renderer = ReactTestRenderer.create(, { + createNodeMock: element => { + return element; + }, + unstable_isConcurrent: true, + })), + ); let nodes = scopeRef.current.DO_NOT_USE_queryAllNodes(testScopeQuery); expect(nodes).toEqual([divRef.current, spanRef.current, aRef.current]); - renderer.update(); + await act(() => renderer.update()); nodes = scopeRef.current.DO_NOT_USE_queryAllNodes(testScopeQuery); expect(nodes).toEqual([aRef.current, divRef.current, spanRef.current]); }); // @gate www - it('DO_NOT_USE_queryFirstNode() works as intended', () => { + it('DO_NOT_USE_queryFirstNode() works as intended', async () => { const testScopeQuery = (type, props) => true; const TestScope = React.unstable_Scope; const scopeRef = React.createRef(); @@ -454,20 +459,26 @@ describe('ReactScope', () => { ); } - const renderer = ReactTestRenderer.create(, { - createNodeMock: element => { - return element; - }, - }); + let renderer; + await act( + () => + (renderer = ReactTestRenderer.create(, { + createNodeMock: element => { + return element; + }, + unstable_isConcurrent: true, + })), + ); let node = scopeRef.current.DO_NOT_USE_queryFirstNode(testScopeQuery); expect(node).toEqual(divRef.current); - renderer.update(); + await act(() => renderer.update()); + node = scopeRef.current.DO_NOT_USE_queryFirstNode(testScopeQuery); expect(node).toEqual(aRef.current); }); // @gate www - it('containsNode() works as intended', () => { + it('containsNode() works as intended', async () => { const TestScope = React.unstable_Scope; const scopeRef = React.createRef(); const divRef = React.createRef(); @@ -500,23 +511,28 @@ describe('ReactScope', () => { ); } - const renderer = ReactTestRenderer.create(, { - createNodeMock: element => { - return element; - }, - }); + let renderer; + await act( + () => + (renderer = ReactTestRenderer.create(, { + createNodeMock: element => { + return element; + }, + unstable_isConcurrent: true, + })), + ); expect(scopeRef.current.containsNode(divRef.current)).toBe(true); expect(scopeRef.current.containsNode(spanRef.current)).toBe(true); expect(scopeRef.current.containsNode(aRef.current)).toBe(true); expect(scopeRef.current.containsNode(outerSpan.current)).toBe(false); expect(scopeRef.current.containsNode(emRef.current)).toBe(false); - renderer.update(); + await act(() => renderer.update()); expect(scopeRef.current.containsNode(divRef.current)).toBe(true); expect(scopeRef.current.containsNode(spanRef.current)).toBe(true); expect(scopeRef.current.containsNode(aRef.current)).toBe(true); expect(scopeRef.current.containsNode(outerSpan.current)).toBe(false); expect(scopeRef.current.containsNode(emRef.current)).toBe(true); - renderer.update(); + await act(() => renderer.update()); expect(scopeRef.current.containsNode(emRef.current)).toBe(false); }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index 14c58cb0d6581..8e1ba771a7dd5 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -378,7 +378,7 @@ describe('ReactSuspense', () => { expect(container.textContent).toEqual('AB'); }); - // @gate forceConcurrentByDefaultForTesting + // @gate !disableLegacyMode && forceConcurrentByDefaultForTesting it( 'interrupts current render when something suspends with a ' + "delay and we've already skipped over a lower priority update in " + diff --git a/packages/react/src/__tests__/ReactJSXRuntime-test.js b/packages/react/src/__tests__/ReactJSXRuntime-test.js index db488e1684001..1c57954b205c4 100644 --- a/packages/react/src/__tests__/ReactJSXRuntime-test.js +++ b/packages/react/src/__tests__/ReactJSXRuntime-test.js @@ -10,12 +10,10 @@ 'use strict'; let React; -let ReactDOM; 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 @@ -27,11 +25,8 @@ describe('ReactJSXRuntime', () => { React = require('react'); JSXRuntime = require('react/jsx-runtime'); JSXDEVRuntime = require('react/jsx-dev-runtime'); - 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 +128,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__; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 67ef3589ca487..dd5f9299e1536 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -88,7 +88,7 @@ const __NEXT_MAJOR__ = __EXPERIMENTAL__; export const enableRefAsProp = __NEXT_MAJOR__; export const disableStringRefs = __NEXT_MAJOR__; export const enableBigIntSupport = __NEXT_MAJOR__; -export const disableLegacyMode = __NEXT_MAJOR__; +export const disableLegacyMode = true; export const disableLegacyContext = __NEXT_MAJOR__; export const disableDOMTestUtils = __NEXT_MAJOR__; export const enableRenderableContext = __NEXT_MAJOR__; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index a1b5dd8dd950a..ef7a207974cf8 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -113,7 +113,7 @@ export const useModernStrictMode = true; // because JSX is an extremely hot path. export const disableStringRefs = false; -export const disableLegacyMode = false; +export const disableLegacyMode = __EXPERIMENTAL__; export const disableDOMTestUtils = false;