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;