@samitouri / QOS-React-1 / commits / f269074723

Revert "Remove module pattern function component support" (#28670)

This breaks internal tests, so must be something in the refactor. Since it's the top commit let's revert and split into two PRs, one that removes the flag and one that does the refactor, so we can find the bug.

Ricky committed Mar 29, 2024 at 10:10 UTC f2690747239533fa266612d2d4dd9ae88ea92fbc
31 files changed +923 -124
packages/react-devtools-shared/src/backend/renderer.js
+1 -1
@@ -225,7 +225,7 @@ export function getInternalReactConstants(version: string): {
225 HostSingleton: 27, // Same as above
226 HostText: 6,
227 IncompleteClassComponent: 17,
228 - IndeterminateComponent: 2, // removed in 19.0.0
228 + IndeterminateComponent: 2,
229 LazyComponent: 16,
230 LegacyHiddenComponent: 23,
231 MemoComponent: 14,
packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js
+68
@@ -14,6 +14,7 @@ let act;
14 let React;
15 let ReactDOM;
16 let ReactDOMClient;
17 +let PropTypes;
18 let findDOMNode;
19
20 const clone = function (o) {
@@ -98,6 +99,7 @@ describe('ReactComponentLifeCycle', () => {
99 findDOMNode =
100 ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.findDOMNode;
101 ReactDOMClient = require('react-dom/client');
102 + PropTypes = require('prop-types');
103 });
104
105 it('should not reuse an instance when it has been unmounted', async () => {
@@ -1112,6 +1114,72 @@ describe('ReactComponentLifeCycle', () => {
1114 });
1115 });
1116
1117 + if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
1118 + // @gate !disableLegacyContext
1119 + it('calls effects on module-pattern component', async () => {
1120 + const log = [];
1121 +
1122 + function Parent() {
1123 + return {
1124 + render() {
1125 + expect(typeof this.props).toBe('object');
1126 + log.push('render');
1127 + return <Child />;
1128 + },
1129 + UNSAFE_componentWillMount() {
1130 + log.push('will mount');
1131 + },
1132 + componentDidMount() {
1133 + log.push('did mount');
1134 + },
1135 + componentDidUpdate() {
1136 + log.push('did update');
1137 + },
1138 + getChildContext() {
1139 + return {x: 2};
1140 + },
1141 + };
1142 + }
1143 + Parent.childContextTypes = {
1144 + x: PropTypes.number,
1145 + };
1146 + function Child(props, context) {
1147 + expect(context.x).toBe(2);
1148 + return <div />;
1149 + }
1150 + Child.contextTypes = {
1151 + x: PropTypes.number,
1152 + };
1153 +
1154 + const root = ReactDOMClient.createRoot(document.createElement('div'));
1155 + await expect(async () => {
1156 + await act(() => {
1157 + root.render(<Parent ref={c => c && log.push('ref')} />);
1158 + });
1159 + }).toErrorDev(
1160 + 'Warning: The <Parent /> component appears to be a function component that returns a class instance. ' +
1161 + 'Change Parent to a class that extends React.Component instead. ' +
1162 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
1163 + '`Parent.prototype = React.Component.prototype`. ' +
1164 + "Don't use an arrow function since it cannot be called with `new` by React.",
1165 + );
1166 + await act(() => {
1167 + root.render(<Parent ref={c => c && log.push('ref')} />);
1168 + });
1169 +
1170 + expect(log).toEqual([
1171 + 'will mount',
1172 + 'render',
1173 + 'did mount',
1174 + 'ref',
1175 +
1176 + 'render',
1177 + 'did update',
1178 + 'ref',
1179 + ]);
1180 + });
1181 + }
1182 +
1183 it('should warn if getDerivedStateFromProps returns undefined', async () => {
1184 class MyComponent extends React.Component {
1185 state = {};
packages/react-dom/src/__tests__/ReactCompositeComponent-test.js
+55 -19
@@ -211,27 +211,63 @@ describe('ReactCompositeComponent', () => {
211 });
212 });
213
214 - it('should not support module pattern components', async () => {
215 - function Child({test}) {
216 - return {
217 - render() {
218 - return <div>{test}</div>;
219 - },
220 - };
221 - }
214 + if (require('shared/ReactFeatureFlags').disableModulePatternComponents) {
215 + it('should not support module pattern components', async () => {
216 + function Child({test}) {
217 + return {
218 + render() {
219 + return <div>{test}</div>;
220 + },
221 + };
222 + }
223
223 - const el = document.createElement('div');
224 - const root = ReactDOMClient.createRoot(el);
225 - await expect(async () => {
226 - await act(() => {
227 - root.render(<Child test="test" />);
228 - });
229 - }).rejects.toThrow(
230 - 'Objects are not valid as a React child (found: object with keys {render}).',
231 - );
224 + const el = document.createElement('div');
225 + const root = ReactDOMClient.createRoot(el);
226 + await expect(async () => {
227 + await expect(async () => {
228 + await act(() => {
229 + root.render(<Child test="test" />);
230 + });
231 + }).rejects.toThrow(
232 + 'Objects are not valid as a React child (found: object with keys {render}).',
233 + );
234 + }).toErrorDev(
235 + 'Warning: The <Child /> component appears to be a function component that returns a class instance. ' +
236 + 'Change Child to a class that extends React.Component instead. ' +
237 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
238 + '`Child.prototype = React.Component.prototype`. ' +
239 + "Don't use an arrow function since it cannot be called with `new` by React.",
240 + );
241
233 - expect(el.textContent).toBe('');
234 - });
242 + expect(el.textContent).toBe('');
243 + });
244 + } else {
245 + it('should support module pattern components', () => {
246 + function Child({test}) {
247 + return {
248 + render() {
249 + return <div>{test}</div>;
250 + },
251 + };
252 + }
253 +
254 + const el = document.createElement('div');
255 + const root = ReactDOMClient.createRoot(el);
256 + expect(() => {
257 + ReactDOM.flushSync(() => {
258 + root.render(<Child test="test" />);
259 + });
260 + }).toErrorDev(
261 + 'Warning: The <Child /> component appears to be a function component that returns a class instance. ' +
262 + 'Change Child to a class that extends React.Component instead. ' +
263 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
264 + '`Child.prototype = React.Component.prototype`. ' +
265 + "Don't use an arrow function since it cannot be called with `new` by React.",
266 + );
267 +
268 + expect(el.textContent).toBe('test');
269 + });
270 + }
271
272 it('should use default values for undefined props', async () => {
273 class Component extends React.Component {
packages/react-dom/src/__tests__/ReactCompositeComponentState-test.js
+66
@@ -527,6 +527,72 @@ describe('ReactCompositeComponent-state', () => {
527 ]);
528 });
529
530 + if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
531 + it('should support stateful module pattern components', async () => {
532 + function Child() {
533 + return {
534 + state: {
535 + count: 123,
536 + },
537 + render() {
538 + return <div>{`count:${this.state.count}`}</div>;
539 + },
540 + };
541 + }
542 +
543 + const el = document.createElement('div');
544 + const root = ReactDOMClient.createRoot(el);
545 + expect(() => {
546 + ReactDOM.flushSync(() => {
547 + root.render(<Child />);
548 + });
549 + }).toErrorDev(
550 + 'Warning: The <Child /> component appears to be a function component that returns a class instance. ' +
551 + 'Change Child to a class that extends React.Component instead. ' +
552 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
553 + '`Child.prototype = React.Component.prototype`. ' +
554 + "Don't use an arrow function since it cannot be called with `new` by React.",
555 + );
556 +
557 + expect(el.textContent).toBe('count:123');
558 + });
559 +
560 + it('should support getDerivedStateFromProps for module pattern components', async () => {
561 + function Child() {
562 + return {
563 + state: {
564 + count: 1,
565 + },
566 + render() {
567 + return <div>{`count:${this.state.count}`}</div>;
568 + },
569 + };
570 + }
571 + Child.getDerivedStateFromProps = (props, prevState) => {
572 + return {
573 + count: prevState.count + props.incrementBy,
574 + };
575 + };
576 +
577 + const el = document.createElement('div');
578 + const root = ReactDOMClient.createRoot(el);
579 + await act(() => {
580 + root.render(<Child incrementBy={0} />);
581 + });
582 +
583 + expect(el.textContent).toBe('count:1');
584 + await act(() => {
585 + root.render(<Child incrementBy={2} />);
586 + });
587 + expect(el.textContent).toBe('count:3');
588 +
589 + await act(() => {
590 + root.render(<Child incrementBy={1} />);
591 + });
592 + expect(el.textContent).toBe('count:4');
593 + });
594 + }
595 +
596 it('should not support setState in componentWillUnmount', async () => {
597 let subscription;
598 class A extends React.Component {
packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js
+20 -7
@@ -627,9 +627,23 @@ describe('ReactDOMServerIntegration', () => {
627 checkFooDiv(await render(<ClassComponent />));
628 });
629
630 - itThrowsWhenRendering(
631 - 'factory components',
632 - async render => {
630 + if (require('shared/ReactFeatureFlags').disableModulePatternComponents) {
631 + itThrowsWhenRendering(
632 + 'factory components',
633 + async render => {
634 + const FactoryComponent = () => {
635 + return {
636 + render: function () {
637 + return <div>foo</div>;
638 + },
639 + };
640 + };
641 + await render(<FactoryComponent />, 1);
642 + },
643 + 'Objects are not valid as a React child (found: object with keys {render})',
644 + );
645 + } else {
646 + itRenders('factory components', async render => {
647 const FactoryComponent = () => {
648 return {
649 render: function () {
@@ -637,10 +651,9 @@ describe('ReactDOMServerIntegration', () => {
651 },
652 };
653 };
640 - await render(<FactoryComponent />, 1);
641 - },
642 - 'Objects are not valid as a React child (found: object with keys {render})',
643 - );
654 + checkFooDiv(await render(<FactoryComponent />, 1));
655 + });
656 + }
657 });
658
659 describe('component hierarchies', function () {
packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+50
@@ -879,6 +879,56 @@ describe('ReactErrorBoundaries', () => {
879 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
880 });
881
882 + // @gate !disableModulePatternComponents
883 + it('renders an error state if module-style context provider throws in componentWillMount', async () => {
884 + function BrokenComponentWillMountWithContext() {
885 + return {
886 + getChildContext() {
887 + return {foo: 42};
888 + },
889 + render() {
890 + return <div>{this.props.children}</div>;
891 + },
892 + UNSAFE_componentWillMount() {
893 + throw new Error('Hello');
894 + },
895 + };
896 + }
897 + BrokenComponentWillMountWithContext.childContextTypes = {
898 + foo: PropTypes.number,
899 + };
900 +
901 + const container = document.createElement('div');
902 + const root = ReactDOMClient.createRoot(container);
903 +
904 + await expect(async () => {
905 + await act(() => {
906 + root.render(
907 + <ErrorBoundary>
908 + <BrokenComponentWillMountWithContext />
909 + </ErrorBoundary>,
910 + );
911 + });
912 + }).toErrorDev([
913 + 'Warning: The <BrokenComponentWillMountWithContext /> component appears to be a function component that ' +
914 + 'returns a class instance. ' +
915 + 'Change BrokenComponentWillMountWithContext to a class that extends React.Component instead. ' +
916 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
917 + '`BrokenComponentWillMountWithContext.prototype = React.Component.prototype`. ' +
918 + "Don't use an arrow function since it cannot be called with `new` by React.",
919 + ...gate(flags =>
920 + flags.disableLegacyContext
921 + ? [
922 + 'Warning: BrokenComponentWillMountWithContext uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.',
923 + 'Warning: BrokenComponentWillMountWithContext uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.',
924 + ]
925 + : [],
926 + ),
927 + ]);
928 +
929 + expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
930 + });
931 +
932 it('mounts the error message if mounting fails', async () => {
933 function renderError(error) {
934 return <ErrorMessage message={error.message} />;
packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js
+48
@@ -849,6 +849,54 @@ describe('ReactLegacyErrorBoundaries', () => {
849 expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
850 });
851
852 + if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
853 + // @gate !disableLegacyMode
854 + it('renders an error state if module-style context provider throws in componentWillMount', () => {
855 + function BrokenComponentWillMountWithContext() {
856 + return {
857 + getChildContext() {
858 + return {foo: 42};
859 + },
860 + render() {
861 + return <div>{this.props.children}</div>;
862 + },
863 + UNSAFE_componentWillMount() {
864 + throw new Error('Hello');
865 + },
866 + };
867 + }
868 + BrokenComponentWillMountWithContext.childContextTypes = {
869 + foo: PropTypes.number,
870 + };
871 +
872 + const container = document.createElement('div');
873 + expect(() =>
874 + ReactDOM.render(
875 + <ErrorBoundary>
876 + <BrokenComponentWillMountWithContext />
877 + </ErrorBoundary>,
878 + container,
879 + ),
880 + ).toErrorDev([
881 + 'Warning: The <BrokenComponentWillMountWithContext /> component appears to be a function component that ' +
882 + 'returns a class instance. ' +
883 + 'Change BrokenComponentWillMountWithContext to a class that extends React.Component instead. ' +
884 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
885 + '`BrokenComponentWillMountWithContext.prototype = React.Component.prototype`. ' +
886 + "Don't use an arrow function since it cannot be called with `new` by React.",
887 + ...gate(flags =>
888 + flags.disableLegacyContext
889 + ? [
890 + 'Warning: BrokenComponentWillMountWithContext uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.',
891 + 'Warning: BrokenComponentWillMountWithContext uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.',
892 + ]
893 + : [],
894 + ),
895 + ]);
896 + expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
897 + });
898 + }
899 +
900 // @gate !disableLegacyMode
901 it('mounts the error message if mounting fails', () => {
902 function renderError(error) {
packages/react-dom/src/__tests__/refs-test.js
+35
@@ -11,6 +11,7 @@
11
12 let React = require('react');
13 let ReactDOMClient = require('react-dom/client');
14 +let ReactFeatureFlags = require('shared/ReactFeatureFlags');
15 let act = require('internal-test-utils').act;
16
17 // This is testing if string refs are deleted from `instance.refs`
@@ -23,6 +24,7 @@ describe('reactiverefs', () => {
24 jest.resetModules();
25 React = require('react');
26 ReactDOMClient = require('react-dom/client');
27 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
28 act = require('internal-test-utils').act;
29 });
30
@@ -193,6 +195,38 @@ describe('reactiverefs', () => {
195 });
196 });
197
198 +if (!ReactFeatureFlags.disableModulePatternComponents) {
199 + describe('factory components', () => {
200 + it('Should correctly get the ref', async () => {
201 + function Comp() {
202 + return {
203 + elemRef: React.createRef(),
204 + render() {
205 + return <div ref={this.elemRef} />;
206 + },
207 + };
208 + }
209 +
210 + let inst;
211 + await expect(async () => {
212 + const container = document.createElement('div');
213 + const root = ReactDOMClient.createRoot(container);
214 +
215 + await act(() => {
216 + root.render(<Comp ref={current => (inst = current)} />);
217 + });
218 + }).toErrorDev(
219 + 'Warning: The <Comp /> component appears to be a function component that returns a class instance. ' +
220 + 'Change Comp to a class that extends React.Component instead. ' +
221 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
222 + '`Comp.prototype = React.Component.prototype`. ' +
223 + "Don't use an arrow function since it cannot be called with `new` by React.",
224 + );
225 + expect(inst.elemRef.current.tagName).toBe('DIV');
226 + });
227 + });
228 +}
229 +
230 /**
231 * Tests that when a ref hops around children, we can track that correctly.
232 */
@@ -202,6 +236,7 @@ describe('ref swapping', () => {
236 jest.resetModules();
237 React = require('react');
238 ReactDOMClient = require('react-dom/client');
239 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
240 act = require('internal-test-utils').act;
241
242 RefHopsAround = class extends React.Component {
packages/react-reconciler/src/ReactFiber.js
+16 -5
@@ -42,6 +42,7 @@ import {
42 import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
43 import {ConcurrentRoot} from './ReactRootTags';
44 import {
45 + IndeterminateComponent,
46 ClassComponent,
47 HostRoot,
48 HostComponent,
@@ -247,10 +248,19 @@ export function isSimpleFunctionComponent(type: any): boolean {
248 );
249 }
250
250 -export function isFunctionClassComponent(
251 - type: (...args: Array<any>) => mixed,
252 -): boolean {
253 - return shouldConstruct(type);
251 +export function resolveLazyComponentTag(Component: Function): WorkTag {
252 + if (typeof Component === 'function') {
253 + return shouldConstruct(Component) ? ClassComponent : FunctionComponent;
254 + } else if (Component !== undefined && Component !== null) {
255 + const $$typeof = Component.$$typeof;
256 + if ($$typeof === REACT_FORWARD_REF_TYPE) {
257 + return ForwardRef;
258 + }
259 + if ($$typeof === REACT_MEMO_TYPE) {
260 + return MemoComponent;
261 + }
262 + }
263 + return IndeterminateComponent;
264 }
265
266 // This is used to create an alternate fiber to do work on.
@@ -341,6 +351,7 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
351 workInProgress._debugInfo = current._debugInfo;
352 workInProgress._debugNeedsRemount = current._debugNeedsRemount;
353 switch (workInProgress.tag) {
354 + case IndeterminateComponent:
355 case FunctionComponent:
356 case SimpleMemoComponent:
357 workInProgress.type = resolveFunctionForHotReloading(current.type);
@@ -481,7 +492,7 @@ export function createFiberFromTypeAndProps(
492 mode: TypeOfMode,
493 lanes: Lanes,
494 ): Fiber {
484 - let fiberTag = FunctionComponent;
495 + let fiberTag = IndeterminateComponent;
496 // The resolved type is set if we know what the final type will be. I.e. it's not lazy.
497 let resolvedType = type;
498 if (typeof type === 'function') {
packages/react-reconciler/src/ReactFiberBeginWork.js
+223 -64
@@ -46,6 +46,7 @@ import {
46 setIsStrictModeForDevtools,
47 } from './ReactFiberDevToolsHook';
48 import {
49 + IndeterminateComponent,
50 FunctionComponent,
51 ClassComponent,
52 HostRoot,
@@ -94,6 +95,7 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
95 import {
96 debugRenderPhaseSideEffectsForStrictMode,
97 disableLegacyContext,
98 + disableModulePatternComponents,
99 enableProfilerCommitHooks,
100 enableProfilerTimer,
101 enableScopeAPI,
@@ -113,12 +115,7 @@ import shallowEqual from 'shared/shallowEqual';
115 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
116 import getComponentNameFromType from 'shared/getComponentNameFromType';
117 import ReactStrictModeWarnings from './ReactStrictModeWarnings';
116 -import {
117 - REACT_LAZY_TYPE,
118 - REACT_FORWARD_REF_TYPE,
119 - REACT_MEMO_TYPE,
120 - getIteratorFn,
121 -} from 'shared/ReactSymbols';
118 +import {REACT_LAZY_TYPE, getIteratorFn} from 'shared/ReactSymbols';
119 import {
120 getCurrentFiberOwnerNameInDevOrNull,
121 setIsRendering,
@@ -239,6 +236,7 @@ import {
236 queueHydrationError,
237 } from './ReactFiberHydrationContext';
238 import {
239 + adoptClassInstance,
240 constructClassInstance,
241 mountClassInstance,
242 resumeMountClassInstance,
@@ -246,12 +244,12 @@ import {
244 } from './ReactFiberClassComponent';
245 import {resolveDefaultProps} from './ReactFiberLazyComponent';
246 import {
247 + resolveLazyComponentTag,
248 createFiberFromTypeAndProps,
249 createFiberFromFragment,
250 createFiberFromOffscreen,
251 createWorkInProgress,
252 isSimpleFunctionComponent,
254 - isFunctionClassComponent,
253 } from './ReactFiber';
254 import {
255 retryDehydratedSuspenseBoundary,
@@ -307,6 +305,7 @@ export const SelectiveHydrationException: mixed = new Error(
305 let didReceiveUpdate: boolean = false;
306
307 let didWarnAboutBadClass;
308 +let didWarnAboutModulePatternComponent;
309 let didWarnAboutContextTypeOnFunctionComponent;
310 let didWarnAboutGetDerivedStateOnFunctionComponent;
311 let didWarnAboutFunctionRefs;
@@ -317,6 +316,7 @@ let didWarnAboutDefaultPropsOnFunctionComponent;
316
317 if (__DEV__) {
318 didWarnAboutBadClass = ({}: {[string]: boolean});
319 + didWarnAboutModulePatternComponent = ({}: {[string]: boolean});
320 didWarnAboutContextTypeOnFunctionComponent = ({}: {[string]: boolean});
321 didWarnAboutGetDerivedStateOnFunctionComponent = ({}: {[string]: boolean});
322 didWarnAboutFunctionRefs = ({}: {[string]: boolean});
@@ -1053,43 +1053,6 @@ function updateFunctionComponent(
1053 nextProps: any,
1054 renderLanes: Lanes,
1055 ) {
1056 - if (__DEV__) {
1057 - if (
1058 - Component.prototype &&
1059 - typeof Component.prototype.render === 'function'
1060 - ) {
1061 - const componentName = getComponentNameFromType(Component) || 'Unknown';
1062 -
1063 - if (!didWarnAboutBadClass[componentName]) {
1064 - console.error(
1065 - "The <%s /> component appears to have a render method, but doesn't extend React.Component. " +
1066 - 'This is likely to cause errors. Change %s to extend React.Component instead.',
1067 - componentName,
1068 - componentName,
1069 - );
1070 - didWarnAboutBadClass[componentName] = true;
1071 - }
1072 - }
1073 -
1074 - if (workInProgress.mode & StrictLegacyMode) {
1075 - ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null);
1076 - }
1077 -
1078 - if (current === null) {
1079 - // Some validations were previously done in mountIndeterminateComponent however and are now run
1080 - // in updateFuntionComponent but only on mount
1081 - validateFunctionComponentInDev(workInProgress, workInProgress.type);
1082 -
1083 - if (disableLegacyContext && Component.contextTypes) {
1084 - console.error(
1085 - '%s uses the legacy contextTypes API which was removed in React 19. ' +
1086 - 'Use React.createContext() with React.useContext() instead.',
1087 - getComponentNameFromType(Component) || 'Unknown',
1088 - );
1089 - }
1090 - }
1091 - }
1092 -
1056 let context;
1057 if (!disableLegacyContext) {
1058 const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
@@ -1736,64 +1699,64 @@ function mountLazyComponent(
1699 let Component = init(payload);
1700 // Store the unwrapped component in the type.
1701 workInProgress.type = Component;
1739 -
1702 + const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component));
1703 const resolvedProps = resolveDefaultProps(Component, props);
1741 - if (typeof Component === 'function') {
1742 - if (isFunctionClassComponent(Component)) {
1743 - workInProgress.tag = ClassComponent;
1704 + let child;
1705 + switch (resolvedTag) {
1706 + case FunctionComponent: {
1707 if (__DEV__) {
1708 + validateFunctionComponentInDev(workInProgress, Component);
1709 workInProgress.type = Component =
1746 - resolveClassForHotReloading(Component);
1710 + resolveFunctionForHotReloading(Component);
1711 }
1748 - return updateClassComponent(
1712 + child = updateFunctionComponent(
1713 null,
1714 workInProgress,
1715 Component,
1716 resolvedProps,
1717 renderLanes,
1718 );
1755 - } else {
1756 - workInProgress.tag = FunctionComponent;
1719 + return child;
1720 + }
1721 + case ClassComponent: {
1722 if (__DEV__) {
1758 - validateFunctionComponentInDev(workInProgress, Component);
1723 workInProgress.type = Component =
1760 - resolveFunctionForHotReloading(Component);
1724 + resolveClassForHotReloading(Component);
1725 }
1762 - return updateFunctionComponent(
1726 + child = updateClassComponent(
1727 null,
1728 workInProgress,
1729 Component,
1730 resolvedProps,
1731 renderLanes,
1732 );
1733 + return child;
1734 }
1770 - } else if (Component !== undefined && Component !== null) {
1771 - const $$typeof = Component.$$typeof;
1772 - if ($$typeof === REACT_FORWARD_REF_TYPE) {
1773 - workInProgress.tag = ForwardRef;
1735 + case ForwardRef: {
1736 if (__DEV__) {
1737 workInProgress.type = Component =
1738 resolveForwardRefForHotReloading(Component);
1739 }
1778 - return updateForwardRef(
1740 + child = updateForwardRef(
1741 null,
1742 workInProgress,
1743 Component,
1744 resolvedProps,
1745 renderLanes,
1746 );
1785 - } else if ($$typeof === REACT_MEMO_TYPE) {
1786 - workInProgress.tag = MemoComponent;
1787 - return updateMemoComponent(
1747 + return child;
1748 + }
1749 + case MemoComponent: {
1750 + child = updateMemoComponent(
1751 null,
1752 workInProgress,
1753 Component,
1754 resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too
1755 renderLanes,
1756 );
1757 + return child;
1758 }
1759 }
1796 -
1760 let hint = '';
1761 if (__DEV__) {
1762 if (
@@ -1853,6 +1816,194 @@ function mountIncompleteClassComponent(
1816 );
1817 }
1818
1819 +function mountIndeterminateComponent(
1820 + _current: null | Fiber,
1821 + workInProgress: Fiber,
1822 + Component: $FlowFixMe,
1823 + renderLanes: Lanes,
1824 +) {
1825 + resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress);
1826 +
1827 + const props = workInProgress.pendingProps;
1828 + let context;
1829 + if (!disableLegacyContext) {
1830 + const unmaskedContext = getUnmaskedContext(
1831 + workInProgress,
1832 + Component,
1833 + false,
1834 + );
1835 + context = getMaskedContext(workInProgress, unmaskedContext);
1836 + }
1837 +
1838 + prepareToReadContext(workInProgress, renderLanes);
1839 + let value;
1840 + let hasId;
1841 +
1842 + if (enableSchedulingProfiler) {
1843 + markComponentRenderStarted(workInProgress);
1844 + }
1845 + if (__DEV__) {
1846 + if (
1847 + Component.prototype &&
1848 + typeof Component.prototype.render === 'function'
1849 + ) {
1850 + const componentName = getComponentNameFromType(Component) || 'Unknown';
1851 +
1852 + if (!didWarnAboutBadClass[componentName]) {
1853 + console.error(
1854 + "The <%s /> component appears to have a render method, but doesn't extend React.Component. " +
1855 + 'This is likely to cause errors. Change %s to extend React.Component instead.',
1856 + componentName,
1857 + componentName,
1858 + );
1859 + didWarnAboutBadClass[componentName] = true;
1860 + }
1861 + }
1862 +
1863 + if (workInProgress.mode & StrictLegacyMode) {
1864 + ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null);
1865 + }
1866 +
1867 + setIsRendering(true);
1868 + ReactCurrentOwner.current = workInProgress;
1869 + value = renderWithHooks(
1870 + null,
1871 + workInProgress,
1872 + Component,
1873 + props,
1874 + context,
1875 + renderLanes,
1876 + );
1877 + hasId = checkDidRenderIdHook();
1878 + setIsRendering(false);
1879 + } else {
1880 + value = renderWithHooks(
1881 + null,
1882 + workInProgress,
1883 + Component,
1884 + props,
1885 + context,
1886 + renderLanes,
1887 + );
1888 + hasId = checkDidRenderIdHook();
1889 + }
1890 + if (enableSchedulingProfiler) {
1891 + markComponentRenderStopped();
1892 + }
1893 +
1894 + // React DevTools reads this flag.
1895 + workInProgress.flags |= PerformedWork;
1896 +
1897 + if (__DEV__) {
1898 + // Support for module components is deprecated and is removed behind a flag.
1899 + // Whether or not it would crash later, we want to show a good message in DEV first.
1900 + if (
1901 + typeof value === 'object' &&
1902 + value !== null &&
1903 + typeof value.render === 'function' &&
1904 + value.$$typeof === undefined
1905 + ) {
1906 + const componentName = getComponentNameFromType(Component) || 'Unknown';
1907 + if (!didWarnAboutModulePatternComponent[componentName]) {
1908 + console.error(
1909 + 'The <%s /> component appears to be a function component that returns a class instance. ' +
1910 + 'Change %s to a class that extends React.Component instead. ' +
1911 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
1912 + "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " +
1913 + 'cannot be called with `new` by React.',
1914 + componentName,
1915 + componentName,
1916 + componentName,
1917 + );
1918 + didWarnAboutModulePatternComponent[componentName] = true;
1919 + }
1920 + }
1921 + }
1922 +
1923 + if (
1924 + // Run these checks in production only if the flag is off.
1925 + // Eventually we'll delete this branch altogether.
1926 + !disableModulePatternComponents &&
1927 + typeof value === 'object' &&
1928 + value !== null &&
1929 + typeof value.render === 'function' &&
1930 + value.$$typeof === undefined
1931 + ) {
1932 + if (__DEV__) {
1933 + const componentName = getComponentNameFromType(Component) || 'Unknown';
1934 + if (!didWarnAboutModulePatternComponent[componentName]) {
1935 + console.error(
1936 + 'The <%s /> component appears to be a function component that returns a class instance. ' +
1937 + 'Change %s to a class that extends React.Component instead. ' +
1938 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
1939 + "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " +
1940 + 'cannot be called with `new` by React.',
1941 + componentName,
1942 + componentName,
1943 + componentName,
1944 + );
1945 + didWarnAboutModulePatternComponent[componentName] = true;
1946 + }
1947 + }
1948 +
1949 + // Proceed under the assumption that this is a class instance
1950 + workInProgress.tag = ClassComponent;
1951 +
1952 + // Throw out any hooks that were used.
1953 + workInProgress.memoizedState = null;
1954 + workInProgress.updateQueue = null;
1955 +
1956 + // Push context providers early to prevent context stack mismatches.
1957 + // During mounting we don't know the child context yet as the instance doesn't exist.
1958 + // We will invalidate the child context in finishClassComponent() right after rendering.
1959 + let hasContext = false;
1960 + if (isLegacyContextProvider(Component)) {
1961 + hasContext = true;
1962 + pushLegacyContextProvider(workInProgress);
1963 + } else {
1964 + hasContext = false;
1965 + }
1966 +
1967 + workInProgress.memoizedState =
1968 + value.state !== null && value.state !== undefined ? value.state : null;
1969 +
1970 + initializeUpdateQueue(workInProgress);
1971 +
1972 + adoptClassInstance(workInProgress, value);
1973 + mountClassInstance(workInProgress, Component, props, renderLanes);
1974 + return finishClassComponent(
1975 + null,
1976 + workInProgress,
1977 + Component,
1978 + true,
1979 + hasContext,
1980 + renderLanes,
1981 + );
1982 + } else {
1983 + // Proceed under the assumption that this is a function component
1984 + workInProgress.tag = FunctionComponent;
1985 + if (__DEV__) {
1986 + if (disableLegacyContext && Component.contextTypes) {
1987 + console.error(
1988 + '%s uses the legacy contextTypes API which was removed in React 19. ' +
1989 + 'Use React.createContext() with React.useContext() instead.',
1990 + getComponentNameFromType(Component) || 'Unknown',
1991 + );
1992 + }
1993 + }
1994 +
1995 + if (getIsHydrating() && hasId) {
1996 + pushMaterializedTreeId(workInProgress);
1997 + }
1998 +
1999 + reconcileChildren(null, workInProgress, value, renderLanes);
2000 + if (__DEV__) {
2001 + validateFunctionComponentInDev(workInProgress, Component);
2002 + }
2003 + return workInProgress.child;
2004 + }
2005 +}
2006 +
2007 function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
2008 if (__DEV__) {
2009 if (Component) {
@@ -3876,6 +4027,14 @@ function beginWork(
4027 workInProgress.lanes = NoLanes;
4028
4029 switch (workInProgress.tag) {
4030 + case IndeterminateComponent: {
4031 + return mountIndeterminateComponent(
4032 + current,
4033 + workInProgress,
4034 + workInProgress.type,
4035 + renderLanes,
4036 + );
4037 + }
4038 case LazyComponent: {
4039 const elementType = workInProgress.elementType;
4040 return mountLazyComponent(
packages/react-reconciler/src/ReactFiberClassComponent.js
+12 -7
@@ -569,6 +569,16 @@ function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) {
569 }
570 }
571
572 +function adoptClassInstance(workInProgress: Fiber, instance: any): void {
573 + instance.updater = classComponentUpdater;
574 + workInProgress.stateNode = instance;
575 + // The instance needs access to the fiber so that it can schedule updates
576 + setInstance(instance, workInProgress);
577 + if (__DEV__) {
578 + instance._reactInternalInstance = fakeInternalInstance;
579 + }
580 +}
581 +
582 function constructClassInstance(
583 workInProgress: Fiber,
584 ctor: any,
@@ -649,13 +659,7 @@ function constructClassInstance(
659 instance.state !== null && instance.state !== undefined
660 ? instance.state
661 : null);
652 - instance.updater = classComponentUpdater;
653 - workInProgress.stateNode = instance;
654 - // The instance needs access to the fiber so that it can schedule updates
655 - setInstance(instance, workInProgress);
656 - if (__DEV__) {
657 - instance._reactInternalInstance = fakeInternalInstance;
658 - }
662 + adoptClassInstance(workInProgress, instance);
663
664 if (__DEV__) {
665 if (typeof ctor.getDerivedStateFromProps === 'function' && state === null) {
@@ -1226,6 +1230,7 @@ function updateClassInstance(
1230 }
1231
1232 export {
1233 + adoptClassInstance,
1234 constructClassInstance,
1235 mountClassInstance,
1236 resumeMountClassInstance,
packages/react-reconciler/src/ReactFiberCompleteWork.js
+2
@@ -45,6 +45,7 @@ import {
45 import {now} from './Scheduler';
46
47 import {
48 + IndeterminateComponent,
49 FunctionComponent,
50 ClassComponent,
51 HostRoot,
@@ -948,6 +949,7 @@ function completeWork(
949 // for hydration.
950 popTreeContext(workInProgress);
951 switch (workInProgress.tag) {
952 + case IndeterminateComponent:
953 case LazyComponent:
954 case SimpleMemoComponent:
955 case FunctionComponent:
packages/react-reconciler/src/ReactFiberComponentStack.js
+2
@@ -17,6 +17,7 @@ import {
17 SuspenseComponent,
18 SuspenseListComponent,
19 FunctionComponent,
20 + IndeterminateComponent,
21 ForwardRef,
22 SimpleMemoComponent,
23 ClassComponent,
@@ -46,6 +47,7 @@ function describeFiber(fiber: Fiber): string {
47 case SuspenseListComponent:
48 return describeBuiltInComponentFrame('SuspenseList', owner);
49 case FunctionComponent:
50 + case IndeterminateComponent:
51 case SimpleMemoComponent:
52 return describeFunctionComponentFrame(fiber.type, owner);
53 case ForwardRef:
packages/react-reconciler/src/ReactFiberHydrationDiffs.js
+2
@@ -17,6 +17,7 @@ import {
17 SuspenseComponent,
18 SuspenseListComponent,
19 FunctionComponent,
20 + IndeterminateComponent,
21 ForwardRef,
22 SimpleMemoComponent,
23 ClassComponent,
@@ -86,6 +87,7 @@ function describeFiberType(fiber: Fiber): null | string {
87 case SuspenseListComponent:
88 return 'SuspenseList';
89 case FunctionComponent:
90 + case IndeterminateComponent:
91 case SimpleMemoComponent:
92 const fn = fiber.type;
93 return fn.displayName || fn.name || null;
packages/react-reconciler/src/ReactFiberWorkLoop.js
+8
@@ -90,6 +90,7 @@ import {
90 } from './ReactTypeOfMode';
91 import {
92 HostRoot,
93 + IndeterminateComponent,
94 ClassComponent,
95 SuspenseComponent,
96 SuspenseListComponent,
@@ -2394,6 +2395,12 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
2395 startProfilerTimer(unitOfWork);
2396 }
2397 switch (unitOfWork.tag) {
2398 + case IndeterminateComponent: {
2399 + // Because it suspended with `use`, we can assume it's a
2400 + // function component.
2401 + unitOfWork.tag = FunctionComponent;
2402 + // Fallthrough to the next branch.
2403 + }
2404 case SimpleMemoComponent:
2405 case FunctionComponent: {
2406 // Resolve `defaultProps`. This logic is copied from `beginWork`.
@@ -3816,6 +3823,7 @@ export function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber: Fiber) {
3823
3824 const tag = fiber.tag;
3825 if (
3826 + tag !== IndeterminateComponent &&
3827 tag !== HostRoot &&
3828 tag !== ClassComponent &&
3829 tag !== FunctionComponent &&
packages/react-reconciler/src/ReactWorkTags.js
+1
@@ -39,6 +39,7 @@ export type WorkTag =
39
40 export const FunctionComponent = 0;
41 export const ClassComponent = 1;
42 +export const IndeterminateComponent = 2; // Before we know whether it is function or class
43 export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
44 export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
45 export const HostComponent = 5;
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+50
@@ -1308,6 +1308,16 @@ describe('ReactHooks', () => {
1308 return <div />;
1309 });
1310
1311 + function Factory() {
1312 + return {
1313 + state: {},
1314 + render() {
1315 + renderCount++;
1316 + return <div />;
1317 + },
1318 + };
1319 + }
1320 +
1321 let renderer;
1322 await act(() => {
1323 renderer = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
@@ -1400,6 +1410,46 @@ describe('ReactHooks', () => {
1410 });
1411 expect(renderCount).toBe(__DEV__ ? 2 : 1);
1412
1413 + if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
1414 + renderCount = 0;
1415 + await expect(async () => {
1416 + await act(() => {
1417 + renderer.update(<Factory />);
1418 + });
1419 + }).toErrorDev(
1420 + 'Warning: The <Factory /> component appears to be a function component that returns a class instance. ' +
1421 + 'Change Factory to a class that extends React.Component instead. ' +
1422 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
1423 + '`Factory.prototype = React.Component.prototype`. ' +
1424 + "Don't use an arrow function since it cannot be called with `new` by React.",
1425 + );
1426 + expect(renderCount).toBe(1);
1427 + renderCount = 0;
1428 + await act(() => {
1429 + renderer.update(<Factory />);
1430 + });
1431 + expect(renderCount).toBe(1);
1432 +
1433 + renderCount = 0;
1434 + await act(() => {
1435 + renderer.update(
1436 + <StrictMode>
1437 + <Factory />
1438 + </StrictMode>,
1439 + );
1440 + });
1441 + expect(renderCount).toBe(__DEV__ ? 2 : 1); // Treated like a class
1442 + renderCount = 0;
1443 + await act(() => {
1444 + renderer.update(
1445 + <StrictMode>
1446 + <Factory />
1447 + </StrictMode>,
1448 + );
1449 + });
1450 + expect(renderCount).toBe(__DEV__ ? 2 : 1); // Treated like a class
1451 + }
1452 +
1453 renderCount = 0;
1454 await act(() => {
1455 renderer.update(<HasHooks />);
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+38
@@ -227,6 +227,44 @@ describe('ReactHooksWithNoopRenderer', () => {
227 await waitForAll([10]);
228 });
229
230 + // @gate !disableModulePatternComponents
231 + it('throws inside module-style components', async () => {
232 + function Counter() {
233 + return {
234 + render() {
235 + const [count] = useState(0);
236 + return <Text text={this.props.label + ': ' + count} />;
237 + },
238 + };
239 + }
240 + ReactNoop.render(<Counter />);
241 + await expect(
242 + async () =>
243 + await waitForThrow(
244 + 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen ' +
245 + 'for one of the following reasons:\n' +
246 + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
247 + '2. You might be breaking the Rules of Hooks\n' +
248 + '3. You might have more than one copy of React in the same app\n' +
249 + 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
250 + ),
251 + ).toErrorDev(
252 + 'Warning: The <Counter /> component appears to be a function component that returns a class instance. ' +
253 + 'Change Counter to a class that extends React.Component instead. ' +
254 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
255 + '`Counter.prototype = React.Component.prototype`. ' +
256 + "Don't use an arrow function since it cannot be called with `new` by React.",
257 + );
258 +
259 + // Confirm that a subsequent hook works properly.
260 + function GoodCounter(props) {
261 + const [count] = useState(props.initialCount);
262 + return <Text text={count} />;
263 + }
264 + ReactNoop.render(<GoodCounter initialCount={10} />);
265 + await waitForAll([10]);
266 + });
267 +
268 it('throws when called outside the render phase', async () => {
269 expect(() => {
270 expect(() => useState(0)).toThrow(
packages/react-reconciler/src/__tests__/ReactIncremental-test.js
+42
@@ -1864,6 +1864,48 @@ describe('ReactIncremental', () => {
1864 ]);
1865 });
1866
1867 + // @gate !disableModulePatternComponents
1868 + // @gate !disableLegacyContext
1869 + it('does not leak own context into context provider (factory components)', async () => {
1870 + function Recurse(props, context) {
1871 + return {
1872 + getChildContext() {
1873 + return {n: (context.n || 3) - 1};
1874 + },
1875 + render() {
1876 + Scheduler.log('Recurse ' + JSON.stringify(context));
1877 + if (context.n === 0) {
1878 + return null;
1879 + }
1880 + return <Recurse />;
1881 + },
1882 + };
1883 + }
1884 + Recurse.contextTypes = {
1885 + n: PropTypes.number,
1886 + };
1887 + Recurse.childContextTypes = {
1888 + n: PropTypes.number,
1889 + };
1890 +
1891 + ReactNoop.render(<Recurse />);
1892 + await expect(
1893 + async () =>
1894 + await waitForAll([
1895 + 'Recurse {}',
1896 + 'Recurse {"n":2}',
1897 + 'Recurse {"n":1}',
1898 + 'Recurse {"n":0}',
1899 + ]),
1900 + ).toErrorDev([
1901 + 'Warning: The <Recurse /> component appears to be a function component that returns a class instance. ' +
1902 + 'Change Recurse to a class that extends React.Component instead. ' +
1903 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
1904 + '`Recurse.prototype = React.Component.prototype`. ' +
1905 + "Don't use an arrow function since it cannot be called with `new` by React.",
1906 + ]);
1907 + });
1908 +
1909 // @gate www
1910 // @gate !disableLegacyContext
1911 it('provides context when reusing work', async () => {
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+39
@@ -1754,6 +1754,45 @@ describe('ReactIncrementalErrorHandling', () => {
1754 );
1755 });
1756
1757 + // @gate !disableModulePatternComponents
1758 + it('handles error thrown inside getDerivedStateFromProps of a module-style context provider', async () => {
1759 + function Provider() {
1760 + return {
1761 + getChildContext() {
1762 + return {foo: 'bar'};
1763 + },
1764 + render() {
1765 + return 'Hi';
1766 + },
1767 + };
1768 + }
1769 + Provider.childContextTypes = {
1770 + x: () => {},
1771 + };
1772 + Provider.getDerivedStateFromProps = () => {
1773 + throw new Error('Oops!');
1774 + };
1775 +
1776 + ReactNoop.render(<Provider />);
1777 + await expect(async () => {
1778 + await waitForThrow('Oops!');
1779 + }).toErrorDev([
1780 + 'Warning: The <Provider /> component appears to be a function component that returns a class instance. ' +
1781 + 'Change Provider to a class that extends React.Component instead. ' +
1782 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
1783 + '`Provider.prototype = React.Component.prototype`. ' +
1784 + "Don't use an arrow function since it cannot be called with `new` by React.",
1785 + ...gate(flags =>
1786 + flags.disableLegacyContext
1787 + ? [
1788 + 'Warning: Provider uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.',
1789 + 'Warning: Provider uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.',
1790 + ]
1791 + : [],
1792 + ),
1793 + ]);
1794 + });
1795 +
1796 it('uncaught errors should be discarded if the render is aborted', async () => {
1797 const root = ReactNoop.createRoot();
1798
packages/react-reconciler/src/__tests__/ReactSubtreeFlagsWarning-test.js
+6 -2
@@ -132,7 +132,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
132
133 // @gate experimental || www
134 it('regression: false positive for legacy suspense', async () => {
135 - const Child = ({text}) => {
135 + // Wrapping in memo because regular function components go through the
136 + // mountIndeterminateComponent path, which acts like there's no `current`
137 + // fiber even though there is. `memo` is not indeterminate, so it goes
138 + // through the update path.
139 + const Child = React.memo(({text}) => {
140 // If text hasn't resolved, this will throw and exit before the passive
141 // static effect flag is added by the useEffect call below.
142 readText(text);
@@ -143,7 +147,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
147
148 Scheduler.log(text);
149 return text;
146 - };
150 + });
151
152 function App() {
153 return (
packages/react-reconciler/src/getComponentNameFromFiber.js
+2
@@ -18,6 +18,7 @@ import {
18 import {
19 FunctionComponent,
20 ClassComponent,
21 + IndeterminateComponent,
22 HostRoot,
23 HostPortal,
24 HostComponent,
@@ -127,6 +128,7 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null {
128 case ClassComponent:
129 case FunctionComponent:
130 case IncompleteClassComponent:
131 + case IndeterminateComponent:
132 case MemoComponent:
133 case SimpleMemoComponent:
134 if (typeof type === 'function') {
packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js
+48
@@ -1639,6 +1639,54 @@ describe('ReactFreshIntegration', () => {
1639 }
1640 });
1641
1642 + if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
1643 + it('remounts deprecated factory components', async () => {
1644 + if (__DEV__) {
1645 + await expect(async () => {
1646 + await render(`
1647 + function Parent() {
1648 + return {
1649 + render() {
1650 + return <Child prop="A" />;
1651 + }
1652 + };
1653 + };
1654 +
1655 + function Child({prop}) {
1656 + return <h1>{prop}1</h1>;
1657 + };
1658 +
1659 + export default Parent;
1660 + `);
1661 + }).toErrorDev(
1662 + 'The <Parent /> component appears to be a function component ' +
1663 + 'that returns a class instance.',
1664 + );
1665 + const el = container.firstChild;
1666 + expect(el.textContent).toBe('A1');
1667 + await patch(`
1668 + function Parent() {
1669 + return {
1670 + render() {
1671 + return <Child prop="B" />;
1672 + }
1673 + };
1674 + };
1675 +
1676 + function Child({prop}) {
1677 + return <h1>{prop}2</h1>;
1678 + };
1679 +
1680 + export default Parent;
1681 + `);
1682 + // Like classes, factory components always remount.
1683 + expect(container.firstChild).not.toBe(el);
1684 + const newEl = container.firstChild;
1685 + expect(newEl.textContent).toBe('B2');
1686 + }
1687 + });
1688 + }
1689 +
1690 describe('with inline requires', () => {
1691 beforeEach(() => {
1692 global.FakeModuleSystem = {};
packages/react-server/src/ReactFizzServer.js
+80 -19
@@ -137,6 +137,7 @@ import {
137 import ReactSharedInternals from 'shared/ReactSharedInternals';
138 import {
139 disableLegacyContext,
140 + disableModulePatternComponents,
141 enableBigIntSupport,
142 enableScopeAPI,
143 enableSuspenseAvoidThisFallbackFizz,
@@ -1387,6 +1388,7 @@ function renderClassComponent(
1388 }
1389
1390 const didWarnAboutBadClass: {[string]: boolean} = {};
1391 +const didWarnAboutModulePatternComponent: {[string]: boolean} = {};
1392 const didWarnAboutContextTypeOnFunctionComponent: {[string]: boolean} = {};
1393 const didWarnAboutGetDerivedStateOnFunctionComponent: {[string]: boolean} = {};
1394 let didWarnAboutReassigningProps = false;
@@ -1394,7 +1396,9 @@ const didWarnAboutDefaultPropsOnFunctionComponent: {[string]: boolean} = {};
1396 let didWarnAboutGenerators = false;
1397 let didWarnAboutMaps = false;
1398
1397 -function renderFunctionComponent(
1399 +// This would typically be a function component but we still support module pattern
1400 +// components for some reason.
1401 +function renderIndeterminateComponent(
1402 request: Request,
1403 task: Task,
1404 keyPath: KeyNode,
@@ -1440,26 +1444,83 @@ function renderFunctionComponent(
1444 const actionStateMatchingIndex = getActionStateMatchingIndex();
1445
1446 if (__DEV__) {
1443 - if (disableLegacyContext && Component.contextTypes) {
1444 - console.error(
1445 - '%s uses the legacy contextTypes API which was removed in React 19. ' +
1446 - 'Use React.createContext() with React.useContext() instead.',
1447 - getComponentNameFromType(Component) || 'Unknown',
1448 - );
1447 + // Support for module components is deprecated and is removed behind a flag.
1448 + // Whether or not it would crash later, we want to show a good message in DEV first.
1449 + if (
1450 + typeof value === 'object' &&
1451 + value !== null &&
1452 + typeof value.render === 'function' &&
1453 + value.$$typeof === undefined
1454 + ) {
1455 + const componentName = getComponentNameFromType(Component) || 'Unknown';
1456 + if (!didWarnAboutModulePatternComponent[componentName]) {
1457 + console.error(
1458 + 'The <%s /> component appears to be a function component that returns a class instance. ' +
1459 + 'Change %s to a class that extends React.Component instead. ' +
1460 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
1461 + "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " +
1462 + 'cannot be called with `new` by React.',
1463 + componentName,
1464 + componentName,
1465 + componentName,
1466 + );
1467 + didWarnAboutModulePatternComponent[componentName] = true;
1468 + }
1469 }
1470 }
1451 - if (__DEV__) {
1452 - validateFunctionComponentInDev(Component);
1471 +
1472 + if (
1473 + // Run these checks in production only if the flag is off.
1474 + // Eventually we'll delete this branch altogether.
1475 + !disableModulePatternComponents &&
1476 + typeof value === 'object' &&
1477 + value !== null &&
1478 + typeof value.render === 'function' &&
1479 + value.$$typeof === undefined
1480 + ) {
1481 + if (__DEV__) {
1482 + const componentName = getComponentNameFromType(Component) || 'Unknown';
1483 + if (!didWarnAboutModulePatternComponent[componentName]) {
1484 + console.error(
1485 + 'The <%s /> component appears to be a function component that returns a class instance. ' +
1486 + 'Change %s to a class that extends React.Component instead. ' +
1487 + "If you can't use a class try assigning the prototype on the function as a workaround. " +
1488 + "`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " +
1489 + 'cannot be called with `new` by React.',
1490 + componentName,
1491 + componentName,
1492 + componentName,
1493 + );
1494 + didWarnAboutModulePatternComponent[componentName] = true;
1495 + }
1496 + }
1497 +
1498 + mountClassInstance(value, Component, props, legacyContext);
1499 + finishClassComponent(request, task, keyPath, value, Component, props);
1500 + } else {
1501 + // Proceed under the assumption that this is a function component
1502 + if (__DEV__) {
1503 + if (disableLegacyContext && Component.contextTypes) {
1504 + console.error(
1505 + '%s uses the legacy contextTypes API which was removed in React 19. ' +
1506 + 'Use React.createContext() with React.useContext() instead.',
1507 + getComponentNameFromType(Component) || 'Unknown',
1508 + );
1509 + }
1510 + }
1511 + if (__DEV__) {
1512 + validateFunctionComponentInDev(Component);
1513 + }
1514 + finishFunctionComponent(
1515 + request,
1516 + task,
1517 + keyPath,
1518 + value,
1519 + hasId,
1520 + actionStateCount,
1521 + actionStateMatchingIndex,
1522 + );
1523 }
1454 - finishFunctionComponent(
1455 - request,
1456 - task,
1457 - keyPath,
1458 - value,
1459 - hasId,
1460 - actionStateCount,
1461 - actionStateMatchingIndex,
1462 - );
1524 task.componentStack = previousComponentStack;
1525 }
1526
@@ -1764,7 +1825,7 @@ function renderElement(
1825 renderClassComponent(request, task, keyPath, type, props);
1826 return;
1827 } else {
1767 - renderFunctionComponent(request, task, keyPath, type, props);
1828 + renderIndeterminateComponent(request, task, keyPath, type, props);
1829 return;
1830 }
1831 }
packages/shared/ReactFeatureFlags.js
+2
@@ -206,6 +206,8 @@ export const enableRenderableContext = __NEXT_MAJOR__;
206 // when we plan to enable them.
207 // -----------------------------------------------------------------------------
208
209 +export const disableModulePatternComponents = __NEXT_MAJOR__;
210 +
211 export const enableUseRefAccessWarning = false;
212
213 // Enables time slicing for updates that aren't wrapped in startTransition.
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -34,6 +34,7 @@ export const {
34 } = dynamicFlags;
35
36 // The rest of the flags are static for better dead code elimination.
37 +export const disableModulePatternComponents = true;
38 export const enableDebugTracing = false;
39 export const enableAsyncDebugInfo = false;
40 export const enableSchedulingProfiler = __PROFILE__;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -31,6 +31,7 @@ export const enableDeferRootSchedulingToMicrotask = __TODO_NEXT_RN_MAJOR__;
31 export const alwaysThrottleRetries = __TODO_NEXT_RN_MAJOR__;
32 export const enableInfiniteRenderLoopDetection = __TODO_NEXT_RN_MAJOR__;
33 export const enableComponentStackLocations = __TODO_NEXT_RN_MAJOR__;
34 +export const disableModulePatternComponents = __TODO_NEXT_RN_MAJOR__;
35
36 // -----------------------------------------------------------------------------
37 // These are ready to flip after the next React npm release (or RN switches to
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -94,6 +94,7 @@ export const disableLegacyMode = __NEXT_MAJOR__;
94 export const disableLegacyContext = __NEXT_MAJOR__;
95 export const disableDOMTestUtils = __NEXT_MAJOR__;
96 export const enableNewBooleanProps = __NEXT_MAJOR__;
97 +export const disableModulePatternComponents = __NEXT_MAJOR__;
98 export const enableRenderableContext = __NEXT_MAJOR__;
99 export const enableReactTestRendererWarning = __NEXT_MAJOR__;
100
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -34,6 +34,7 @@ export const enableSuspenseCallback = false;
34 export const disableLegacyContext = false;
35 export const enableTrustedTypesIntegration = false;
36 export const disableTextareaChildren = false;
37 +export const disableModulePatternComponents = true;
38 export const enableComponentStackLocations = false;
39 export const enableLegacyFBSupport = false;
40 export const enableFilterEmptyStringAttributesDOM = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -34,6 +34,7 @@ export const enableSuspenseCallback = true;
34 export const disableLegacyContext = false;
35 export const enableTrustedTypesIntegration = false;
36 export const disableTextareaChildren = false;
37 +export const disableModulePatternComponents = true;
38 export const enableSuspenseAvoidThisFallback = true;
39 export const enableSuspenseAvoidThisFallbackFizz = false;
40 export const enableCPUSuspense = false;
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -82,6 +82,8 @@ export const enablePostpone = false;
82 // Need to remove it.
83 export const disableCommentsAsDOMContainers = false;
84
85 +export const disableModulePatternComponents = true;
86 +
87 export const enableCreateEventHandleAPI = true;
88
89 export const enableScopeAPI = true;