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;