From f2690747239533fa266612d2d4dd9ae88ea92fbc Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 29 Mar 2024 10:10:11 -0400 Subject: [PATCH] Revert "Remove module pattern function component support" (#28670) This breaks internal tests, so must be something in the refactor. Since it's the top commit let's revert and split into two PRs, one that removes the flag and one that does the refactor, so we can find the bug. --- .../src/backend/renderer.js | 2 +- .../__tests__/ReactComponentLifeCycle-test.js | 68 +++++ .../__tests__/ReactCompositeComponent-test.js | 74 +++-- .../ReactCompositeComponentState-test.js | 66 ++++ .../ReactDOMServerIntegrationElements-test.js | 27 +- .../ReactErrorBoundaries-test.internal.js | 50 +++ ...eactLegacyErrorBoundaries-test.internal.js | 48 +++ packages/react-dom/src/__tests__/refs-test.js | 35 +++ packages/react-reconciler/src/ReactFiber.js | 21 +- .../src/ReactFiberBeginWork.js | 287 ++++++++++++++---- .../src/ReactFiberClassComponent.js | 19 +- .../src/ReactFiberCompleteWork.js | 2 + .../src/ReactFiberComponentStack.js | 2 + .../src/ReactFiberHydrationDiffs.js | 2 + .../src/ReactFiberWorkLoop.js | 8 + .../react-reconciler/src/ReactWorkTags.js | 1 + .../src/__tests__/ReactHooks-test.internal.js | 50 +++ .../ReactHooksWithNoopRenderer-test.js | 38 +++ .../src/__tests__/ReactIncremental-test.js | 42 +++ ...tIncrementalErrorHandling-test.internal.js | 39 +++ .../ReactSubtreeFlagsWarning-test.js | 8 +- .../src/getComponentNameFromFiber.js | 2 + .../__tests__/ReactFreshIntegration-test.js | 48 +++ packages/react-server/src/ReactFizzServer.js | 99 ++++-- packages/shared/ReactFeatureFlags.js | 2 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.native.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 2 + 31 files changed, 923 insertions(+), 124 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 58a6717615492..b1e5dac527e61 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -225,7 +225,7 @@ export function getInternalReactConstants(version: string): { HostSingleton: 27, // Same as above HostText: 6, IncompleteClassComponent: 17, - IndeterminateComponent: 2, // removed in 19.0.0 + IndeterminateComponent: 2, LazyComponent: 16, LegacyHiddenComponent: 23, MemoComponent: 14, diff --git a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js index f89e95bcf5dde..d07ab9e71b0b3 100644 --- a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js +++ b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js @@ -14,6 +14,7 @@ let act; let React; let ReactDOM; let ReactDOMClient; +let PropTypes; let findDOMNode; const clone = function (o) { @@ -98,6 +99,7 @@ describe('ReactComponentLifeCycle', () => { findDOMNode = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.findDOMNode; ReactDOMClient = require('react-dom/client'); + PropTypes = require('prop-types'); }); it('should not reuse an instance when it has been unmounted', async () => { @@ -1112,6 +1114,72 @@ describe('ReactComponentLifeCycle', () => { }); }); + if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) { + // @gate !disableLegacyContext + it('calls effects on module-pattern component', async () => { + const log = []; + + function Parent() { + return { + render() { + expect(typeof this.props).toBe('object'); + log.push('render'); + return ; + }, + UNSAFE_componentWillMount() { + log.push('will mount'); + }, + componentDidMount() { + log.push('did mount'); + }, + componentDidUpdate() { + log.push('did update'); + }, + getChildContext() { + return {x: 2}; + }, + }; + } + Parent.childContextTypes = { + x: PropTypes.number, + }; + function Child(props, context) { + expect(context.x).toBe(2); + return
; + } + Child.contextTypes = { + x: PropTypes.number, + }; + + const root = ReactDOMClient.createRoot(document.createElement('div')); + await expect(async () => { + await act(() => { + root.render( c && log.push('ref')} />); + }); + }).toErrorDev( + 'Warning: The component appears to be a function component that returns a class instance. ' + + 'Change Parent to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + '`Parent.prototype = React.Component.prototype`. ' + + "Don't use an arrow function since it cannot be called with `new` by React.", + ); + await act(() => { + root.render( c && log.push('ref')} />); + }); + + expect(log).toEqual([ + 'will mount', + 'render', + 'did mount', + 'ref', + + 'render', + 'did update', + 'ref', + ]); + }); + } + it('should warn if getDerivedStateFromProps returns undefined', async () => { class MyComponent extends React.Component { state = {}; diff --git a/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js b/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js index 2e56a911a0c38..561928b24faf3 100644 --- a/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js @@ -211,27 +211,63 @@ describe('ReactCompositeComponent', () => { }); }); - it('should not support module pattern components', async () => { - function Child({test}) { - return { - render() { - return
{test}
; - }, - }; - } + if (require('shared/ReactFeatureFlags').disableModulePatternComponents) { + it('should not support module pattern components', async () => { + function Child({test}) { + return { + render() { + return
{test}
; + }, + }; + } - const el = document.createElement('div'); - const root = ReactDOMClient.createRoot(el); - await expect(async () => { - await act(() => { - root.render(); - }); - }).rejects.toThrow( - 'Objects are not valid as a React child (found: object with keys {render}).', - ); + const el = document.createElement('div'); + const root = ReactDOMClient.createRoot(el); + await expect(async () => { + await expect(async () => { + await act(() => { + root.render(); + }); + }).rejects.toThrow( + 'Objects are not valid as a React child (found: object with keys {render}).', + ); + }).toErrorDev( + 'Warning: The component appears to be a function component that returns a class instance. ' + + 'Change Child to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + '`Child.prototype = React.Component.prototype`. ' + + "Don't use an arrow function since it cannot be called with `new` by React.", + ); - expect(el.textContent).toBe(''); - }); + expect(el.textContent).toBe(''); + }); + } else { + it('should support module pattern components', () => { + function Child({test}) { + return { + render() { + return
{test}
; + }, + }; + } + + const el = document.createElement('div'); + const root = ReactDOMClient.createRoot(el); + expect(() => { + ReactDOM.flushSync(() => { + root.render(); + }); + }).toErrorDev( + 'Warning: The component appears to be a function component that returns a class instance. ' + + 'Change Child to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + '`Child.prototype = React.Component.prototype`. ' + + "Don't use an arrow function since it cannot be called with `new` by React.", + ); + + expect(el.textContent).toBe('test'); + }); + } it('should use default values for undefined props', async () => { class Component extends React.Component { diff --git a/packages/react-dom/src/__tests__/ReactCompositeComponentState-test.js b/packages/react-dom/src/__tests__/ReactCompositeComponentState-test.js index ecb30f0f1d78e..a1d3d28533fe9 100644 --- a/packages/react-dom/src/__tests__/ReactCompositeComponentState-test.js +++ b/packages/react-dom/src/__tests__/ReactCompositeComponentState-test.js @@ -527,6 +527,72 @@ describe('ReactCompositeComponent-state', () => { ]); }); + if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) { + it('should support stateful module pattern components', async () => { + function Child() { + return { + state: { + count: 123, + }, + render() { + return
{`count:${this.state.count}`}
; + }, + }; + } + + const el = document.createElement('div'); + const root = ReactDOMClient.createRoot(el); + expect(() => { + ReactDOM.flushSync(() => { + root.render(); + }); + }).toErrorDev( + 'Warning: The component appears to be a function component that returns a class instance. ' + + 'Change Child to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + '`Child.prototype = React.Component.prototype`. ' + + "Don't use an arrow function since it cannot be called with `new` by React.", + ); + + expect(el.textContent).toBe('count:123'); + }); + + it('should support getDerivedStateFromProps for module pattern components', async () => { + function Child() { + return { + state: { + count: 1, + }, + render() { + return
{`count:${this.state.count}`}
; + }, + }; + } + Child.getDerivedStateFromProps = (props, prevState) => { + return { + count: prevState.count + props.incrementBy, + }; + }; + + const el = document.createElement('div'); + const root = ReactDOMClient.createRoot(el); + await act(() => { + root.render(); + }); + + expect(el.textContent).toBe('count:1'); + await act(() => { + root.render(); + }); + expect(el.textContent).toBe('count:3'); + + await act(() => { + root.render(); + }); + expect(el.textContent).toBe('count:4'); + }); + } + it('should not support setState in componentWillUnmount', async () => { let subscription; class A extends React.Component { diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js index f492aebb455db..a66cd12cd9178 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js @@ -627,9 +627,23 @@ describe('ReactDOMServerIntegration', () => { checkFooDiv(await render()); }); - itThrowsWhenRendering( - 'factory components', - async render => { + if (require('shared/ReactFeatureFlags').disableModulePatternComponents) { + itThrowsWhenRendering( + 'factory components', + async render => { + const FactoryComponent = () => { + return { + render: function () { + return
foo
; + }, + }; + }; + await render(, 1); + }, + 'Objects are not valid as a React child (found: object with keys {render})', + ); + } else { + itRenders('factory components', async render => { const FactoryComponent = () => { return { render: function () { @@ -637,10 +651,9 @@ describe('ReactDOMServerIntegration', () => { }, }; }; - await render(, 1); - }, - 'Objects are not valid as a React child (found: object with keys {render})', - ); + checkFooDiv(await render(, 1)); + }); + } }); describe('component hierarchies', function () { diff --git a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js index ffa923de3de58..36a227c0fabab 100644 --- a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js @@ -879,6 +879,56 @@ describe('ReactErrorBoundaries', () => { expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); }); + // @gate !disableModulePatternComponents + it('renders an error state if module-style context provider throws in componentWillMount', async () => { + function BrokenComponentWillMountWithContext() { + return { + getChildContext() { + return {foo: 42}; + }, + render() { + return
{this.props.children}
; + }, + UNSAFE_componentWillMount() { + throw new Error('Hello'); + }, + }; + } + BrokenComponentWillMountWithContext.childContextTypes = { + foo: PropTypes.number, + }; + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await expect(async () => { + await act(() => { + root.render( + + + , + ); + }); + }).toErrorDev([ + 'Warning: The component appears to be a function component that ' + + 'returns a class instance. ' + + 'Change BrokenComponentWillMountWithContext to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + '`BrokenComponentWillMountWithContext.prototype = React.Component.prototype`. ' + + "Don't use an arrow function since it cannot be called with `new` by React.", + ...gate(flags => + flags.disableLegacyContext + ? [ + 'Warning: BrokenComponentWillMountWithContext uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.', + 'Warning: BrokenComponentWillMountWithContext uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.', + ] + : [], + ), + ]); + + expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); + }); + it('mounts the error message if mounting fails', async () => { function renderError(error) { return ; diff --git a/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js b/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js index b0b223dd43bee..8c53de16bf814 100644 --- a/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js @@ -849,6 +849,54 @@ describe('ReactLegacyErrorBoundaries', () => { expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); }); + if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) { + // @gate !disableLegacyMode + it('renders an error state if module-style context provider throws in componentWillMount', () => { + function BrokenComponentWillMountWithContext() { + return { + getChildContext() { + return {foo: 42}; + }, + render() { + return
{this.props.children}
; + }, + UNSAFE_componentWillMount() { + throw new Error('Hello'); + }, + }; + } + BrokenComponentWillMountWithContext.childContextTypes = { + foo: PropTypes.number, + }; + + const container = document.createElement('div'); + expect(() => + ReactDOM.render( + + + , + container, + ), + ).toErrorDev([ + 'Warning: The component appears to be a function component that ' + + 'returns a class instance. ' + + 'Change BrokenComponentWillMountWithContext to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + '`BrokenComponentWillMountWithContext.prototype = React.Component.prototype`. ' + + "Don't use an arrow function since it cannot be called with `new` by React.", + ...gate(flags => + flags.disableLegacyContext + ? [ + 'Warning: BrokenComponentWillMountWithContext uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.', + 'Warning: BrokenComponentWillMountWithContext uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.', + ] + : [], + ), + ]); + expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); + }); + } + // @gate !disableLegacyMode it('mounts the error message if mounting fails', () => { function renderError(error) { diff --git a/packages/react-dom/src/__tests__/refs-test.js b/packages/react-dom/src/__tests__/refs-test.js index 4a638ef17c566..f43ada19e004f 100644 --- a/packages/react-dom/src/__tests__/refs-test.js +++ b/packages/react-dom/src/__tests__/refs-test.js @@ -11,6 +11,7 @@ let React = require('react'); let ReactDOMClient = require('react-dom/client'); +let ReactFeatureFlags = require('shared/ReactFeatureFlags'); let act = require('internal-test-utils').act; // This is testing if string refs are deleted from `instance.refs` @@ -23,6 +24,7 @@ describe('reactiverefs', () => { jest.resetModules(); React = require('react'); ReactDOMClient = require('react-dom/client'); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); act = require('internal-test-utils').act; }); @@ -193,6 +195,38 @@ describe('reactiverefs', () => { }); }); +if (!ReactFeatureFlags.disableModulePatternComponents) { + describe('factory components', () => { + it('Should correctly get the ref', async () => { + function Comp() { + return { + elemRef: React.createRef(), + render() { + return
; + }, + }; + } + + let inst; + await expect(async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render( (inst = current)} />); + }); + }).toErrorDev( + 'Warning: The component appears to be a function component that returns a class instance. ' + + 'Change Comp to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + '`Comp.prototype = React.Component.prototype`. ' + + "Don't use an arrow function since it cannot be called with `new` by React.", + ); + expect(inst.elemRef.current.tagName).toBe('DIV'); + }); + }); +} + /** * Tests that when a ref hops around children, we can track that correctly. */ @@ -202,6 +236,7 @@ describe('ref swapping', () => { jest.resetModules(); React = require('react'); ReactDOMClient = require('react-dom/client'); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); act = require('internal-test-utils').act; RefHopsAround = class extends React.Component { diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index dbc2b42a7ea85..ce909b802530a 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -42,6 +42,7 @@ import { import {NoFlags, Placement, StaticMask} from './ReactFiberFlags'; import {ConcurrentRoot} from './ReactRootTags'; import { + IndeterminateComponent, ClassComponent, HostRoot, HostComponent, @@ -247,10 +248,19 @@ export function isSimpleFunctionComponent(type: any): boolean { ); } -export function isFunctionClassComponent( - type: (...args: Array) => mixed, -): boolean { - return shouldConstruct(type); +export function resolveLazyComponentTag(Component: Function): WorkTag { + if (typeof Component === 'function') { + return shouldConstruct(Component) ? ClassComponent : FunctionComponent; + } else if (Component !== undefined && Component !== null) { + const $$typeof = Component.$$typeof; + if ($$typeof === REACT_FORWARD_REF_TYPE) { + return ForwardRef; + } + if ($$typeof === REACT_MEMO_TYPE) { + return MemoComponent; + } + } + return IndeterminateComponent; } // This is used to create an alternate fiber to do work on. @@ -341,6 +351,7 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { workInProgress._debugInfo = current._debugInfo; workInProgress._debugNeedsRemount = current._debugNeedsRemount; switch (workInProgress.tag) { + case IndeterminateComponent: case FunctionComponent: case SimpleMemoComponent: workInProgress.type = resolveFunctionForHotReloading(current.type); @@ -481,7 +492,7 @@ export function createFiberFromTypeAndProps( mode: TypeOfMode, lanes: Lanes, ): Fiber { - let fiberTag = FunctionComponent; + let fiberTag = IndeterminateComponent; // The resolved type is set if we know what the final type will be. I.e. it's not lazy. let resolvedType = type; if (typeof type === 'function') { diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 6e5f53edb796b..c53e01d4ec2c6 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -46,6 +46,7 @@ import { setIsStrictModeForDevtools, } from './ReactFiberDevToolsHook'; import { + IndeterminateComponent, FunctionComponent, ClassComponent, HostRoot, @@ -94,6 +95,7 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; import { debugRenderPhaseSideEffectsForStrictMode, disableLegacyContext, + disableModulePatternComponents, enableProfilerCommitHooks, enableProfilerTimer, enableScopeAPI, @@ -113,12 +115,7 @@ import shallowEqual from 'shared/shallowEqual'; import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; import getComponentNameFromType from 'shared/getComponentNameFromType'; import ReactStrictModeWarnings from './ReactStrictModeWarnings'; -import { - REACT_LAZY_TYPE, - REACT_FORWARD_REF_TYPE, - REACT_MEMO_TYPE, - getIteratorFn, -} from 'shared/ReactSymbols'; +import {REACT_LAZY_TYPE, getIteratorFn} from 'shared/ReactSymbols'; import { getCurrentFiberOwnerNameInDevOrNull, setIsRendering, @@ -239,6 +236,7 @@ import { queueHydrationError, } from './ReactFiberHydrationContext'; import { + adoptClassInstance, constructClassInstance, mountClassInstance, resumeMountClassInstance, @@ -246,12 +244,12 @@ import { } from './ReactFiberClassComponent'; import {resolveDefaultProps} from './ReactFiberLazyComponent'; import { + resolveLazyComponentTag, createFiberFromTypeAndProps, createFiberFromFragment, createFiberFromOffscreen, createWorkInProgress, isSimpleFunctionComponent, - isFunctionClassComponent, } from './ReactFiber'; import { retryDehydratedSuspenseBoundary, @@ -307,6 +305,7 @@ export const SelectiveHydrationException: mixed = new Error( let didReceiveUpdate: boolean = false; let didWarnAboutBadClass; +let didWarnAboutModulePatternComponent; let didWarnAboutContextTypeOnFunctionComponent; let didWarnAboutGetDerivedStateOnFunctionComponent; let didWarnAboutFunctionRefs; @@ -317,6 +316,7 @@ let didWarnAboutDefaultPropsOnFunctionComponent; if (__DEV__) { didWarnAboutBadClass = ({}: {[string]: boolean}); + didWarnAboutModulePatternComponent = ({}: {[string]: boolean}); didWarnAboutContextTypeOnFunctionComponent = ({}: {[string]: boolean}); didWarnAboutGetDerivedStateOnFunctionComponent = ({}: {[string]: boolean}); didWarnAboutFunctionRefs = ({}: {[string]: boolean}); @@ -1053,43 +1053,6 @@ function updateFunctionComponent( nextProps: any, renderLanes: Lanes, ) { - if (__DEV__) { - if ( - Component.prototype && - typeof Component.prototype.render === 'function' - ) { - const componentName = getComponentNameFromType(Component) || 'Unknown'; - - if (!didWarnAboutBadClass[componentName]) { - console.error( - "The <%s /> component appears to have a render method, but doesn't extend React.Component. " + - 'This is likely to cause errors. Change %s to extend React.Component instead.', - componentName, - componentName, - ); - didWarnAboutBadClass[componentName] = true; - } - } - - if (workInProgress.mode & StrictLegacyMode) { - ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null); - } - - if (current === null) { - // Some validations were previously done in mountIndeterminateComponent however and are now run - // in updateFuntionComponent but only on mount - validateFunctionComponentInDev(workInProgress, workInProgress.type); - - if (disableLegacyContext && Component.contextTypes) { - console.error( - '%s uses the legacy contextTypes API which was removed in React 19. ' + - 'Use React.createContext() with React.useContext() instead.', - getComponentNameFromType(Component) || 'Unknown', - ); - } - } - } - let context; if (!disableLegacyContext) { const unmaskedContext = getUnmaskedContext(workInProgress, Component, true); @@ -1736,64 +1699,64 @@ function mountLazyComponent( let Component = init(payload); // Store the unwrapped component in the type. workInProgress.type = Component; - + const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component)); const resolvedProps = resolveDefaultProps(Component, props); - if (typeof Component === 'function') { - if (isFunctionClassComponent(Component)) { - workInProgress.tag = ClassComponent; + let child; + switch (resolvedTag) { + case FunctionComponent: { if (__DEV__) { + validateFunctionComponentInDev(workInProgress, Component); workInProgress.type = Component = - resolveClassForHotReloading(Component); + resolveFunctionForHotReloading(Component); } - return updateClassComponent( + child = updateFunctionComponent( null, workInProgress, Component, resolvedProps, renderLanes, ); - } else { - workInProgress.tag = FunctionComponent; + return child; + } + case ClassComponent: { if (__DEV__) { - validateFunctionComponentInDev(workInProgress, Component); workInProgress.type = Component = - resolveFunctionForHotReloading(Component); + resolveClassForHotReloading(Component); } - return updateFunctionComponent( + child = updateClassComponent( null, workInProgress, Component, resolvedProps, renderLanes, ); + return child; } - } else if (Component !== undefined && Component !== null) { - const $$typeof = Component.$$typeof; - if ($$typeof === REACT_FORWARD_REF_TYPE) { - workInProgress.tag = ForwardRef; + case ForwardRef: { if (__DEV__) { workInProgress.type = Component = resolveForwardRefForHotReloading(Component); } - return updateForwardRef( + child = updateForwardRef( null, workInProgress, Component, resolvedProps, renderLanes, ); - } else if ($$typeof === REACT_MEMO_TYPE) { - workInProgress.tag = MemoComponent; - return updateMemoComponent( + return child; + } + case MemoComponent: { + child = updateMemoComponent( null, workInProgress, Component, resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too renderLanes, ); + return child; } } - let hint = ''; if (__DEV__) { if ( @@ -1853,6 +1816,194 @@ function mountIncompleteClassComponent( ); } +function mountIndeterminateComponent( + _current: null | Fiber, + workInProgress: Fiber, + Component: $FlowFixMe, + renderLanes: Lanes, +) { + resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress); + + const props = workInProgress.pendingProps; + let context; + if (!disableLegacyContext) { + const unmaskedContext = getUnmaskedContext( + workInProgress, + Component, + false, + ); + context = getMaskedContext(workInProgress, unmaskedContext); + } + + prepareToReadContext(workInProgress, renderLanes); + let value; + let hasId; + + if (enableSchedulingProfiler) { + markComponentRenderStarted(workInProgress); + } + if (__DEV__) { + if ( + Component.prototype && + typeof Component.prototype.render === 'function' + ) { + const componentName = getComponentNameFromType(Component) || 'Unknown'; + + if (!didWarnAboutBadClass[componentName]) { + console.error( + "The <%s /> component appears to have a render method, but doesn't extend React.Component. " + + 'This is likely to cause errors. Change %s to extend React.Component instead.', + componentName, + componentName, + ); + didWarnAboutBadClass[componentName] = true; + } + } + + if (workInProgress.mode & StrictLegacyMode) { + ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null); + } + + setIsRendering(true); + ReactCurrentOwner.current = workInProgress; + value = renderWithHooks( + null, + workInProgress, + Component, + props, + context, + renderLanes, + ); + hasId = checkDidRenderIdHook(); + setIsRendering(false); + } else { + value = renderWithHooks( + null, + workInProgress, + Component, + props, + context, + renderLanes, + ); + hasId = checkDidRenderIdHook(); + } + if (enableSchedulingProfiler) { + markComponentRenderStopped(); + } + + // React DevTools reads this flag. + workInProgress.flags |= PerformedWork; + + if (__DEV__) { + // Support for module components is deprecated and is removed behind a flag. + // Whether or not it would crash later, we want to show a good message in DEV first. + if ( + typeof value === 'object' && + value !== null && + typeof value.render === 'function' && + value.$$typeof === undefined + ) { + const componentName = getComponentNameFromType(Component) || 'Unknown'; + if (!didWarnAboutModulePatternComponent[componentName]) { + console.error( + 'The <%s /> component appears to be a function component that returns a class instance. ' + + 'Change %s to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " + + 'cannot be called with `new` by React.', + componentName, + componentName, + componentName, + ); + didWarnAboutModulePatternComponent[componentName] = true; + } + } + } + + if ( + // Run these checks in production only if the flag is off. + // Eventually we'll delete this branch altogether. + !disableModulePatternComponents && + typeof value === 'object' && + value !== null && + typeof value.render === 'function' && + value.$$typeof === undefined + ) { + if (__DEV__) { + const componentName = getComponentNameFromType(Component) || 'Unknown'; + if (!didWarnAboutModulePatternComponent[componentName]) { + console.error( + 'The <%s /> component appears to be a function component that returns a class instance. ' + + 'Change %s to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " + + 'cannot be called with `new` by React.', + componentName, + componentName, + componentName, + ); + didWarnAboutModulePatternComponent[componentName] = true; + } + } + + // Proceed under the assumption that this is a class instance + workInProgress.tag = ClassComponent; + + // Throw out any hooks that were used. + workInProgress.memoizedState = null; + workInProgress.updateQueue = null; + + // Push context providers early to prevent context stack mismatches. + // During mounting we don't know the child context yet as the instance doesn't exist. + // We will invalidate the child context in finishClassComponent() right after rendering. + let hasContext = false; + if (isLegacyContextProvider(Component)) { + hasContext = true; + pushLegacyContextProvider(workInProgress); + } else { + hasContext = false; + } + + workInProgress.memoizedState = + value.state !== null && value.state !== undefined ? value.state : null; + + initializeUpdateQueue(workInProgress); + + adoptClassInstance(workInProgress, value); + mountClassInstance(workInProgress, Component, props, renderLanes); + return finishClassComponent( + null, + workInProgress, + Component, + true, + hasContext, + renderLanes, + ); + } else { + // Proceed under the assumption that this is a function component + workInProgress.tag = FunctionComponent; + if (__DEV__) { + if (disableLegacyContext && Component.contextTypes) { + console.error( + '%s uses the legacy contextTypes API which was removed in React 19. ' + + 'Use React.createContext() with React.useContext() instead.', + getComponentNameFromType(Component) || 'Unknown', + ); + } + } + + if (getIsHydrating() && hasId) { + pushMaterializedTreeId(workInProgress); + } + + reconcileChildren(null, workInProgress, value, renderLanes); + if (__DEV__) { + validateFunctionComponentInDev(workInProgress, Component); + } + return workInProgress.child; + } +} + function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) { if (__DEV__) { if (Component) { @@ -3876,6 +4027,14 @@ function beginWork( workInProgress.lanes = NoLanes; switch (workInProgress.tag) { + case IndeterminateComponent: { + return mountIndeterminateComponent( + current, + workInProgress, + workInProgress.type, + renderLanes, + ); + } case LazyComponent: { const elementType = workInProgress.elementType; return mountLazyComponent( diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 47d1c3cfee476..231e6a3508e4e 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -569,6 +569,16 @@ function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) { } } +function adoptClassInstance(workInProgress: Fiber, instance: any): void { + instance.updater = classComponentUpdater; + workInProgress.stateNode = instance; + // The instance needs access to the fiber so that it can schedule updates + setInstance(instance, workInProgress); + if (__DEV__) { + instance._reactInternalInstance = fakeInternalInstance; + } +} + function constructClassInstance( workInProgress: Fiber, ctor: any, @@ -649,13 +659,7 @@ function constructClassInstance( instance.state !== null && instance.state !== undefined ? instance.state : null); - instance.updater = classComponentUpdater; - workInProgress.stateNode = instance; - // The instance needs access to the fiber so that it can schedule updates - setInstance(instance, workInProgress); - if (__DEV__) { - instance._reactInternalInstance = fakeInternalInstance; - } + adoptClassInstance(workInProgress, instance); if (__DEV__) { if (typeof ctor.getDerivedStateFromProps === 'function' && state === null) { @@ -1226,6 +1230,7 @@ function updateClassInstance( } export { + adoptClassInstance, constructClassInstance, mountClassInstance, resumeMountClassInstance, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index a07a9739016a9..89044182672ad 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -45,6 +45,7 @@ import { import {now} from './Scheduler'; import { + IndeterminateComponent, FunctionComponent, ClassComponent, HostRoot, @@ -948,6 +949,7 @@ function completeWork( // for hydration. popTreeContext(workInProgress); switch (workInProgress.tag) { + case IndeterminateComponent: case LazyComponent: case SimpleMemoComponent: case FunctionComponent: diff --git a/packages/react-reconciler/src/ReactFiberComponentStack.js b/packages/react-reconciler/src/ReactFiberComponentStack.js index f292cb51d10b4..36e22e8a9b1f2 100644 --- a/packages/react-reconciler/src/ReactFiberComponentStack.js +++ b/packages/react-reconciler/src/ReactFiberComponentStack.js @@ -17,6 +17,7 @@ import { SuspenseComponent, SuspenseListComponent, FunctionComponent, + IndeterminateComponent, ForwardRef, SimpleMemoComponent, ClassComponent, @@ -46,6 +47,7 @@ function describeFiber(fiber: Fiber): string { case SuspenseListComponent: return describeBuiltInComponentFrame('SuspenseList', owner); case FunctionComponent: + case IndeterminateComponent: case SimpleMemoComponent: return describeFunctionComponentFrame(fiber.type, owner); case ForwardRef: diff --git a/packages/react-reconciler/src/ReactFiberHydrationDiffs.js b/packages/react-reconciler/src/ReactFiberHydrationDiffs.js index 021da8abf33f1..812d9d046a533 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationDiffs.js +++ b/packages/react-reconciler/src/ReactFiberHydrationDiffs.js @@ -17,6 +17,7 @@ import { SuspenseComponent, SuspenseListComponent, FunctionComponent, + IndeterminateComponent, ForwardRef, SimpleMemoComponent, ClassComponent, @@ -86,6 +87,7 @@ function describeFiberType(fiber: Fiber): null | string { case SuspenseListComponent: return 'SuspenseList'; case FunctionComponent: + case IndeterminateComponent: case SimpleMemoComponent: const fn = fiber.type; return fn.displayName || fn.name || null; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 41a7c8d7efa4d..f9b18aff86485 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -90,6 +90,7 @@ import { } from './ReactTypeOfMode'; import { HostRoot, + IndeterminateComponent, ClassComponent, SuspenseComponent, SuspenseListComponent, @@ -2394,6 +2395,12 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void { startProfilerTimer(unitOfWork); } switch (unitOfWork.tag) { + case IndeterminateComponent: { + // Because it suspended with `use`, we can assume it's a + // function component. + unitOfWork.tag = FunctionComponent; + // Fallthrough to the next branch. + } case SimpleMemoComponent: case FunctionComponent: { // Resolve `defaultProps`. This logic is copied from `beginWork`. @@ -3816,6 +3823,7 @@ export function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber: Fiber) { const tag = fiber.tag; if ( + tag !== IndeterminateComponent && tag !== HostRoot && tag !== ClassComponent && tag !== FunctionComponent && diff --git a/packages/react-reconciler/src/ReactWorkTags.js b/packages/react-reconciler/src/ReactWorkTags.js index bc6782b02f610..8e928d671dc87 100644 --- a/packages/react-reconciler/src/ReactWorkTags.js +++ b/packages/react-reconciler/src/ReactWorkTags.js @@ -39,6 +39,7 @@ export type WorkTag = export const FunctionComponent = 0; export const ClassComponent = 1; +export const IndeterminateComponent = 2; // Before we know whether it is function or class export const HostRoot = 3; // Root of a host tree. Could be nested inside another node. export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer. export const HostComponent = 5; diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index b63a8b23476e4..5bfa66d66fab9 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -1308,6 +1308,16 @@ describe('ReactHooks', () => { return
; }); + function Factory() { + return { + state: {}, + render() { + renderCount++; + return
; + }, + }; + } + let renderer; await act(() => { renderer = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); @@ -1400,6 +1410,46 @@ describe('ReactHooks', () => { }); expect(renderCount).toBe(__DEV__ ? 2 : 1); + if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) { + renderCount = 0; + await expect(async () => { + await act(() => { + renderer.update(); + }); + }).toErrorDev( + 'Warning: The component appears to be a function component that returns a class instance. ' + + 'Change Factory to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + '`Factory.prototype = React.Component.prototype`. ' + + "Don't use an arrow function since it cannot be called with `new` by React.", + ); + expect(renderCount).toBe(1); + renderCount = 0; + await act(() => { + renderer.update(); + }); + expect(renderCount).toBe(1); + + renderCount = 0; + await act(() => { + renderer.update( + + + , + ); + }); + expect(renderCount).toBe(__DEV__ ? 2 : 1); // Treated like a class + renderCount = 0; + await act(() => { + renderer.update( + + + , + ); + }); + expect(renderCount).toBe(__DEV__ ? 2 : 1); // Treated like a class + } + renderCount = 0; await act(() => { renderer.update(); diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index 04e7be86c61cf..45b223e8106a4 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -227,6 +227,44 @@ describe('ReactHooksWithNoopRenderer', () => { await waitForAll([10]); }); + // @gate !disableModulePatternComponents + it('throws inside module-style components', async () => { + function Counter() { + return { + render() { + const [count] = useState(0); + return ; + }, + }; + } + ReactNoop.render(); + await expect( + async () => + await waitForThrow( + 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen ' + + 'for one of the following reasons:\n' + + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + + '2. You might be breaking the Rules of Hooks\n' + + '3. You might have more than one copy of React in the same app\n' + + 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.', + ), + ).toErrorDev( + 'Warning: The component appears to be a function component that returns a class instance. ' + + 'Change Counter to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + '`Counter.prototype = React.Component.prototype`. ' + + "Don't use an arrow function since it cannot be called with `new` by React.", + ); + + // Confirm that a subsequent hook works properly. + function GoodCounter(props) { + const [count] = useState(props.initialCount); + return ; + } + ReactNoop.render(); + await waitForAll([10]); + }); + it('throws when called outside the render phase', async () => { expect(() => { expect(() => useState(0)).toThrow( diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.js index 4beb0a12dabb2..13f904bf9d014 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.js @@ -1864,6 +1864,48 @@ describe('ReactIncremental', () => { ]); }); + // @gate !disableModulePatternComponents + // @gate !disableLegacyContext + it('does not leak own context into context provider (factory components)', async () => { + function Recurse(props, context) { + return { + getChildContext() { + return {n: (context.n || 3) - 1}; + }, + render() { + Scheduler.log('Recurse ' + JSON.stringify(context)); + if (context.n === 0) { + return null; + } + return ; + }, + }; + } + Recurse.contextTypes = { + n: PropTypes.number, + }; + Recurse.childContextTypes = { + n: PropTypes.number, + }; + + ReactNoop.render(); + await expect( + async () => + await waitForAll([ + 'Recurse {}', + 'Recurse {"n":2}', + 'Recurse {"n":1}', + 'Recurse {"n":0}', + ]), + ).toErrorDev([ + 'Warning: The component appears to be a function component that returns a class instance. ' + + 'Change Recurse to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + '`Recurse.prototype = React.Component.prototype`. ' + + "Don't use an arrow function since it cannot be called with `new` by React.", + ]); + }); + // @gate www // @gate !disableLegacyContext it('provides context when reusing work', async () => { diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js index c6e342871b198..6d86507d5bdec 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js @@ -1754,6 +1754,45 @@ describe('ReactIncrementalErrorHandling', () => { ); }); + // @gate !disableModulePatternComponents + it('handles error thrown inside getDerivedStateFromProps of a module-style context provider', async () => { + function Provider() { + return { + getChildContext() { + return {foo: 'bar'}; + }, + render() { + return 'Hi'; + }, + }; + } + Provider.childContextTypes = { + x: () => {}, + }; + Provider.getDerivedStateFromProps = () => { + throw new Error('Oops!'); + }; + + ReactNoop.render(); + await expect(async () => { + await waitForThrow('Oops!'); + }).toErrorDev([ + 'Warning: The component appears to be a function component that returns a class instance. ' + + 'Change Provider to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + '`Provider.prototype = React.Component.prototype`. ' + + "Don't use an arrow function since it cannot be called with `new` by React.", + ...gate(flags => + flags.disableLegacyContext + ? [ + 'Warning: Provider uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.', + 'Warning: Provider uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.', + ] + : [], + ), + ]); + }); + it('uncaught errors should be discarded if the render is aborted', async () => { const root = ReactNoop.createRoot(); diff --git a/packages/react-reconciler/src/__tests__/ReactSubtreeFlagsWarning-test.js b/packages/react-reconciler/src/__tests__/ReactSubtreeFlagsWarning-test.js index e5d9c9c445dad..49bde67837cdf 100644 --- a/packages/react-reconciler/src/__tests__/ReactSubtreeFlagsWarning-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSubtreeFlagsWarning-test.js @@ -132,7 +132,11 @@ describe('ReactSuspenseWithNoopRenderer', () => { // @gate experimental || www it('regression: false positive for legacy suspense', async () => { - const Child = ({text}) => { + // Wrapping in memo because regular function components go through the + // mountIndeterminateComponent path, which acts like there's no `current` + // fiber even though there is. `memo` is not indeterminate, so it goes + // through the update path. + const Child = React.memo(({text}) => { // If text hasn't resolved, this will throw and exit before the passive // static effect flag is added by the useEffect call below. readText(text); @@ -143,7 +147,7 @@ describe('ReactSuspenseWithNoopRenderer', () => { Scheduler.log(text); return text; - }; + }); function App() { return ( diff --git a/packages/react-reconciler/src/getComponentNameFromFiber.js b/packages/react-reconciler/src/getComponentNameFromFiber.js index 9eb7fdf4f8907..1a8464835ce4f 100644 --- a/packages/react-reconciler/src/getComponentNameFromFiber.js +++ b/packages/react-reconciler/src/getComponentNameFromFiber.js @@ -18,6 +18,7 @@ import { import { FunctionComponent, ClassComponent, + IndeterminateComponent, HostRoot, HostPortal, HostComponent, @@ -127,6 +128,7 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null { case ClassComponent: case FunctionComponent: case IncompleteClassComponent: + case IndeterminateComponent: case MemoComponent: case SimpleMemoComponent: if (typeof type === 'function') { diff --git a/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js b/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js index c1e5f308b2202..ed8c56072a217 100644 --- a/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js +++ b/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js @@ -1639,6 +1639,54 @@ describe('ReactFreshIntegration', () => { } }); + if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) { + it('remounts deprecated factory components', async () => { + if (__DEV__) { + await expect(async () => { + await render(` + function Parent() { + return { + render() { + return ; + } + }; + }; + + function Child({prop}) { + return

{prop}1

; + }; + + export default Parent; + `); + }).toErrorDev( + 'The component appears to be a function component ' + + 'that returns a class instance.', + ); + const el = container.firstChild; + expect(el.textContent).toBe('A1'); + await patch(` + function Parent() { + return { + render() { + return ; + } + }; + }; + + function Child({prop}) { + return

{prop}2

; + }; + + export default Parent; + `); + // Like classes, factory components always remount. + expect(container.firstChild).not.toBe(el); + const newEl = container.firstChild; + expect(newEl.textContent).toBe('B2'); + } + }); + } + describe('with inline requires', () => { beforeEach(() => { global.FakeModuleSystem = {}; diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 4567a1e80d13b..c66608414d900 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -137,6 +137,7 @@ import { import ReactSharedInternals from 'shared/ReactSharedInternals'; import { disableLegacyContext, + disableModulePatternComponents, enableBigIntSupport, enableScopeAPI, enableSuspenseAvoidThisFallbackFizz, @@ -1387,6 +1388,7 @@ function renderClassComponent( } const didWarnAboutBadClass: {[string]: boolean} = {}; +const didWarnAboutModulePatternComponent: {[string]: boolean} = {}; const didWarnAboutContextTypeOnFunctionComponent: {[string]: boolean} = {}; const didWarnAboutGetDerivedStateOnFunctionComponent: {[string]: boolean} = {}; let didWarnAboutReassigningProps = false; @@ -1394,7 +1396,9 @@ const didWarnAboutDefaultPropsOnFunctionComponent: {[string]: boolean} = {}; let didWarnAboutGenerators = false; let didWarnAboutMaps = false; -function renderFunctionComponent( +// This would typically be a function component but we still support module pattern +// components for some reason. +function renderIndeterminateComponent( request: Request, task: Task, keyPath: KeyNode, @@ -1440,26 +1444,83 @@ function renderFunctionComponent( const actionStateMatchingIndex = getActionStateMatchingIndex(); if (__DEV__) { - if (disableLegacyContext && Component.contextTypes) { - console.error( - '%s uses the legacy contextTypes API which was removed in React 19. ' + - 'Use React.createContext() with React.useContext() instead.', - getComponentNameFromType(Component) || 'Unknown', - ); + // Support for module components is deprecated and is removed behind a flag. + // Whether or not it would crash later, we want to show a good message in DEV first. + if ( + typeof value === 'object' && + value !== null && + typeof value.render === 'function' && + value.$$typeof === undefined + ) { + const componentName = getComponentNameFromType(Component) || 'Unknown'; + if (!didWarnAboutModulePatternComponent[componentName]) { + console.error( + 'The <%s /> component appears to be a function component that returns a class instance. ' + + 'Change %s to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " + + 'cannot be called with `new` by React.', + componentName, + componentName, + componentName, + ); + didWarnAboutModulePatternComponent[componentName] = true; + } } } - if (__DEV__) { - validateFunctionComponentInDev(Component); + + if ( + // Run these checks in production only if the flag is off. + // Eventually we'll delete this branch altogether. + !disableModulePatternComponents && + typeof value === 'object' && + value !== null && + typeof value.render === 'function' && + value.$$typeof === undefined + ) { + if (__DEV__) { + const componentName = getComponentNameFromType(Component) || 'Unknown'; + if (!didWarnAboutModulePatternComponent[componentName]) { + console.error( + 'The <%s /> component appears to be a function component that returns a class instance. ' + + 'Change %s to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " + + 'cannot be called with `new` by React.', + componentName, + componentName, + componentName, + ); + didWarnAboutModulePatternComponent[componentName] = true; + } + } + + mountClassInstance(value, Component, props, legacyContext); + finishClassComponent(request, task, keyPath, value, Component, props); + } else { + // Proceed under the assumption that this is a function component + if (__DEV__) { + if (disableLegacyContext && Component.contextTypes) { + console.error( + '%s uses the legacy contextTypes API which was removed in React 19. ' + + 'Use React.createContext() with React.useContext() instead.', + getComponentNameFromType(Component) || 'Unknown', + ); + } + } + if (__DEV__) { + validateFunctionComponentInDev(Component); + } + finishFunctionComponent( + request, + task, + keyPath, + value, + hasId, + actionStateCount, + actionStateMatchingIndex, + ); } - finishFunctionComponent( - request, - task, - keyPath, - value, - hasId, - actionStateCount, - actionStateMatchingIndex, - ); task.componentStack = previousComponentStack; } @@ -1764,7 +1825,7 @@ function renderElement( renderClassComponent(request, task, keyPath, type, props); return; } else { - renderFunctionComponent(request, task, keyPath, type, props); + renderIndeterminateComponent(request, task, keyPath, type, props); return; } } diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 3861ea5b6329c..9747dd38f6691 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -206,6 +206,8 @@ export const enableRenderableContext = __NEXT_MAJOR__; // when we plan to enable them. // ----------------------------------------------------------------------------- +export const disableModulePatternComponents = __NEXT_MAJOR__; + export const enableUseRefAccessWarning = false; // Enables time slicing for updates that aren't wrapped in startTransition. diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 1c6180ae903ae..e51541b1bb93d 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -34,6 +34,7 @@ export const { } = dynamicFlags; // The rest of the flags are static for better dead code elimination. +export const disableModulePatternComponents = true; export const enableDebugTracing = false; export const enableAsyncDebugInfo = false; export const enableSchedulingProfiler = __PROFILE__; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 1c3a95b52c40b..d447207b98e80 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -31,6 +31,7 @@ export const enableDeferRootSchedulingToMicrotask = __TODO_NEXT_RN_MAJOR__; export const alwaysThrottleRetries = __TODO_NEXT_RN_MAJOR__; export const enableInfiniteRenderLoopDetection = __TODO_NEXT_RN_MAJOR__; export const enableComponentStackLocations = __TODO_NEXT_RN_MAJOR__; +export const disableModulePatternComponents = __TODO_NEXT_RN_MAJOR__; // ----------------------------------------------------------------------------- // These are ready to flip after the next React npm release (or RN switches to diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index d5b60e8203396..bce10070683f6 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -94,6 +94,7 @@ export const disableLegacyMode = __NEXT_MAJOR__; export const disableLegacyContext = __NEXT_MAJOR__; export const disableDOMTestUtils = __NEXT_MAJOR__; export const enableNewBooleanProps = __NEXT_MAJOR__; +export const disableModulePatternComponents = __NEXT_MAJOR__; export const enableRenderableContext = __NEXT_MAJOR__; export const enableReactTestRendererWarning = __NEXT_MAJOR__; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 710eeb607ebfb..b184d47d8fe3a 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -34,6 +34,7 @@ export const enableSuspenseCallback = false; export const disableLegacyContext = false; export const enableTrustedTypesIntegration = false; export const disableTextareaChildren = false; +export const disableModulePatternComponents = true; export const enableComponentStackLocations = false; export const enableLegacyFBSupport = false; export const enableFilterEmptyStringAttributesDOM = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index a4a3c138a218d..87b5e0302aea2 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -34,6 +34,7 @@ export const enableSuspenseCallback = true; export const disableLegacyContext = false; export const enableTrustedTypesIntegration = false; export const disableTextareaChildren = false; +export const disableModulePatternComponents = true; export const enableSuspenseAvoidThisFallback = true; export const enableSuspenseAvoidThisFallbackFizz = false; export const enableCPUSuspense = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index c309500c3f00b..92c3eb0b38653 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -82,6 +82,8 @@ export const enablePostpone = false; // Need to remove it. export const disableCommentsAsDOMContainers = false; +export const disableModulePatternComponents = true; + export const enableCreateEventHandleAPI = true; export const enableScopeAPI = true;