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;