Remove module pattern function component support (#27742)

The module pattern

```
function MyComponent() {
  return {
    render() {
      return this.state.foo
    }
  }
}
```

has been deprecated for approximately 5 years now. This PR removes
support for this pattern. It also simplifies a number of code paths in
particular related to the concept of `IndeterminateComponent` types.
This commit is contained in:
Josh Story
2024-03-28 13:08:08 -07:00
committed by GitHub
parent 63651c49e0
commit cc56bed38c
31 changed files with 131 additions and 930 deletions
+1 -1
View File
@@ -225,7 +225,7 @@ export function getInternalReactConstants(version: string): {
HostSingleton: 27, // Same as above
HostText: 6,
IncompleteClassComponent: 17,
IndeterminateComponent: 2,
IndeterminateComponent: 2, // removed in 19.0.0
LazyComponent: 16,
LegacyHiddenComponent: 23,
MemoComponent: 14,
@@ -14,7 +14,6 @@ let act;
let React;
let ReactDOM;
let ReactDOMClient;
let PropTypes;
let findDOMNode;
const clone = function (o) {
@@ -99,7 +98,6 @@ describe('ReactComponentLifeCycle', () => {
findDOMNode =
ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.findDOMNode;
ReactDOMClient = require('react-dom/client');
PropTypes = require('prop-types');
});
it('should not reuse an instance when it has been unmounted', async () => {
@@ -1114,72 +1112,6 @@ describe('ReactComponentLifeCycle', () => {
});
});
if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
// @gate !disableLegacyContext
it('calls effects on module-pattern component', async () => {
const log = [];
function Parent() {
return {
render() {
expect(typeof this.props).toBe('object');
log.push('render');
return <Child />;
},
UNSAFE_componentWillMount() {
log.push('will mount');
},
componentDidMount() {
log.push('did mount');
},
componentDidUpdate() {
log.push('did update');
},
getChildContext() {
return {x: 2};
},
};
}
Parent.childContextTypes = {
x: PropTypes.number,
};
function Child(props, context) {
expect(context.x).toBe(2);
return <div />;
}
Child.contextTypes = {
x: PropTypes.number,
};
const root = ReactDOMClient.createRoot(document.createElement('div'));
await expect(async () => {
await act(() => {
root.render(<Parent ref={c => c && log.push('ref')} />);
});
}).toErrorDev(
'Warning: The <Parent /> component appears to be a function component that returns a class instance. ' +
'Change Parent to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
'`Parent.prototype = React.Component.prototype`. ' +
"Don't use an arrow function since it cannot be called with `new` by React.",
);
await act(() => {
root.render(<Parent ref={c => c && log.push('ref')} />);
});
expect(log).toEqual([
'will mount',
'render',
'did mount',
'ref',
'render',
'did update',
'ref',
]);
});
}
it('should warn if getDerivedStateFromProps returns undefined', async () => {
class MyComponent extends React.Component {
state = {};
@@ -211,63 +211,27 @@ describe('ReactCompositeComponent', () => {
});
});
if (require('shared/ReactFeatureFlags').disableModulePatternComponents) {
it('should not support module pattern components', async () => {
function Child({test}) {
return {
render() {
return <div>{test}</div>;
},
};
}
it('should not support module pattern components', async () => {
function Child({test}) {
return {
render() {
return <div>{test}</div>;
},
};
}
const el = document.createElement('div');
const root = ReactDOMClient.createRoot(el);
await expect(async () => {
await expect(async () => {
await act(() => {
root.render(<Child test="test" />);
});
}).rejects.toThrow(
'Objects are not valid as a React child (found: object with keys {render}).',
);
}).toErrorDev(
'Warning: The <Child /> component appears to be a function component that returns a class instance. ' +
'Change Child to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
'`Child.prototype = React.Component.prototype`. ' +
"Don't use an arrow function since it cannot be called with `new` by React.",
);
const el = document.createElement('div');
const root = ReactDOMClient.createRoot(el);
await expect(async () => {
await act(() => {
root.render(<Child test="test" />);
});
}).rejects.toThrow(
'Objects are not valid as a React child (found: object with keys {render}).',
);
expect(el.textContent).toBe('');
});
} else {
it('should support module pattern components', () => {
function Child({test}) {
return {
render() {
return <div>{test}</div>;
},
};
}
const el = document.createElement('div');
const root = ReactDOMClient.createRoot(el);
expect(() => {
ReactDOM.flushSync(() => {
root.render(<Child test="test" />);
});
}).toErrorDev(
'Warning: The <Child /> component appears to be a function component that returns a class instance. ' +
'Change Child to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
'`Child.prototype = React.Component.prototype`. ' +
"Don't use an arrow function since it cannot be called with `new` by React.",
);
expect(el.textContent).toBe('test');
});
}
expect(el.textContent).toBe('');
});
it('should use default values for undefined props', async () => {
class Component extends React.Component {
@@ -527,72 +527,6 @@ describe('ReactCompositeComponent-state', () => {
]);
});
if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
it('should support stateful module pattern components', async () => {
function Child() {
return {
state: {
count: 123,
},
render() {
return <div>{`count:${this.state.count}`}</div>;
},
};
}
const el = document.createElement('div');
const root = ReactDOMClient.createRoot(el);
expect(() => {
ReactDOM.flushSync(() => {
root.render(<Child />);
});
}).toErrorDev(
'Warning: The <Child /> component appears to be a function component that returns a class instance. ' +
'Change Child to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
'`Child.prototype = React.Component.prototype`. ' +
"Don't use an arrow function since it cannot be called with `new` by React.",
);
expect(el.textContent).toBe('count:123');
});
it('should support getDerivedStateFromProps for module pattern components', async () => {
function Child() {
return {
state: {
count: 1,
},
render() {
return <div>{`count:${this.state.count}`}</div>;
},
};
}
Child.getDerivedStateFromProps = (props, prevState) => {
return {
count: prevState.count + props.incrementBy,
};
};
const el = document.createElement('div');
const root = ReactDOMClient.createRoot(el);
await act(() => {
root.render(<Child incrementBy={0} />);
});
expect(el.textContent).toBe('count:1');
await act(() => {
root.render(<Child incrementBy={2} />);
});
expect(el.textContent).toBe('count:3');
await act(() => {
root.render(<Child incrementBy={1} />);
});
expect(el.textContent).toBe('count:4');
});
}
it('should not support setState in componentWillUnmount', async () => {
let subscription;
class A extends React.Component {
@@ -627,23 +627,9 @@ describe('ReactDOMServerIntegration', () => {
checkFooDiv(await render(<ClassComponent />));
});
if (require('shared/ReactFeatureFlags').disableModulePatternComponents) {
itThrowsWhenRendering(
'factory components',
async render => {
const FactoryComponent = () => {
return {
render: function () {
return <div>foo</div>;
},
};
};
await render(<FactoryComponent />, 1);
},
'Objects are not valid as a React child (found: object with keys {render})',
);
} else {
itRenders('factory components', async render => {
itThrowsWhenRendering(
'factory components',
async render => {
const FactoryComponent = () => {
return {
render: function () {
@@ -651,9 +637,10 @@ describe('ReactDOMServerIntegration', () => {
},
};
};
checkFooDiv(await render(<FactoryComponent />, 1));
});
}
await render(<FactoryComponent />, 1);
},
'Objects are not valid as a React child (found: object with keys {render})',
);
});
describe('component hierarchies', function () {
@@ -879,56 +879,6 @@ describe('ReactErrorBoundaries', () => {
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
});
// @gate !disableModulePatternComponents
it('renders an error state if module-style context provider throws in componentWillMount', async () => {
function BrokenComponentWillMountWithContext() {
return {
getChildContext() {
return {foo: 42};
},
render() {
return <div>{this.props.children}</div>;
},
UNSAFE_componentWillMount() {
throw new Error('Hello');
},
};
}
BrokenComponentWillMountWithContext.childContextTypes = {
foo: PropTypes.number,
};
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(
<ErrorBoundary>
<BrokenComponentWillMountWithContext />
</ErrorBoundary>,
);
});
}).toErrorDev([
'Warning: The <BrokenComponentWillMountWithContext /> component appears to be a function component that ' +
'returns a class instance. ' +
'Change BrokenComponentWillMountWithContext to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
'`BrokenComponentWillMountWithContext.prototype = React.Component.prototype`. ' +
"Don't use an arrow function since it cannot be called with `new` by React.",
...gate(flags =>
flags.disableLegacyContext
? [
'Warning: BrokenComponentWillMountWithContext uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.',
'Warning: BrokenComponentWillMountWithContext uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.',
]
: [],
),
]);
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
});
it('mounts the error message if mounting fails', async () => {
function renderError(error) {
return <ErrorMessage message={error.message} />;
@@ -849,54 +849,6 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
});
if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
// @gate !disableLegacyMode
it('renders an error state if module-style context provider throws in componentWillMount', () => {
function BrokenComponentWillMountWithContext() {
return {
getChildContext() {
return {foo: 42};
},
render() {
return <div>{this.props.children}</div>;
},
UNSAFE_componentWillMount() {
throw new Error('Hello');
},
};
}
BrokenComponentWillMountWithContext.childContextTypes = {
foo: PropTypes.number,
};
const container = document.createElement('div');
expect(() =>
ReactDOM.render(
<ErrorBoundary>
<BrokenComponentWillMountWithContext />
</ErrorBoundary>,
container,
),
).toErrorDev([
'Warning: The <BrokenComponentWillMountWithContext /> component appears to be a function component that ' +
'returns a class instance. ' +
'Change BrokenComponentWillMountWithContext to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
'`BrokenComponentWillMountWithContext.prototype = React.Component.prototype`. ' +
"Don't use an arrow function since it cannot be called with `new` by React.",
...gate(flags =>
flags.disableLegacyContext
? [
'Warning: BrokenComponentWillMountWithContext uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.',
'Warning: BrokenComponentWillMountWithContext uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.',
]
: [],
),
]);
expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
});
}
// @gate !disableLegacyMode
it('mounts the error message if mounting fails', () => {
function renderError(error) {
-35
View File
@@ -11,7 +11,6 @@
let React = require('react');
let ReactDOMClient = require('react-dom/client');
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
let act = require('internal-test-utils').act;
// This is testing if string refs are deleted from `instance.refs`
@@ -24,7 +23,6 @@ describe('reactiverefs', () => {
jest.resetModules();
React = require('react');
ReactDOMClient = require('react-dom/client');
ReactFeatureFlags = require('shared/ReactFeatureFlags');
act = require('internal-test-utils').act;
});
@@ -195,38 +193,6 @@ describe('reactiverefs', () => {
});
});
if (!ReactFeatureFlags.disableModulePatternComponents) {
describe('factory components', () => {
it('Should correctly get the ref', async () => {
function Comp() {
return {
elemRef: React.createRef(),
render() {
return <div ref={this.elemRef} />;
},
};
}
let inst;
await expect(async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Comp ref={current => (inst = current)} />);
});
}).toErrorDev(
'Warning: The <Comp /> component appears to be a function component that returns a class instance. ' +
'Change Comp to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
'`Comp.prototype = React.Component.prototype`. ' +
"Don't use an arrow function since it cannot be called with `new` by React.",
);
expect(inst.elemRef.current.tagName).toBe('DIV');
});
});
}
/**
* Tests that when a ref hops around children, we can track that correctly.
*/
@@ -236,7 +202,6 @@ describe('ref swapping', () => {
jest.resetModules();
React = require('react');
ReactDOMClient = require('react-dom/client');
ReactFeatureFlags = require('shared/ReactFeatureFlags');
act = require('internal-test-utils').act;
RefHopsAround = class extends React.Component {
+5 -16
View File
@@ -42,7 +42,6 @@ import {
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
import {ConcurrentRoot} from './ReactRootTags';
import {
IndeterminateComponent,
ClassComponent,
HostRoot,
HostComponent,
@@ -248,19 +247,10 @@ export function isSimpleFunctionComponent(type: any): boolean {
);
}
export function resolveLazyComponentTag(Component: Function): WorkTag {
if (typeof Component === 'function') {
return shouldConstruct(Component) ? ClassComponent : FunctionComponent;
} else if (Component !== undefined && Component !== null) {
const $$typeof = Component.$$typeof;
if ($$typeof === REACT_FORWARD_REF_TYPE) {
return ForwardRef;
}
if ($$typeof === REACT_MEMO_TYPE) {
return MemoComponent;
}
}
return IndeterminateComponent;
export function isFunctionClassComponent(
type: (...args: Array<any>) => mixed,
): boolean {
return shouldConstruct(type);
}
// This is used to create an alternate fiber to do work on.
@@ -351,7 +341,6 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
workInProgress._debugInfo = current._debugInfo;
workInProgress._debugNeedsRemount = current._debugNeedsRemount;
switch (workInProgress.tag) {
case IndeterminateComponent:
case FunctionComponent:
case SimpleMemoComponent:
workInProgress.type = resolveFunctionForHotReloading(current.type);
@@ -492,7 +481,7 @@ export function createFiberFromTypeAndProps(
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let fiberTag = IndeterminateComponent;
let fiberTag = FunctionComponent;
// The resolved type is set if we know what the final type will be. I.e. it's not lazy.
let resolvedType = type;
if (typeof type === 'function') {
+71 -230
View File
@@ -46,7 +46,6 @@ import {
setIsStrictModeForDevtools,
} from './ReactFiberDevToolsHook';
import {
IndeterminateComponent,
FunctionComponent,
ClassComponent,
HostRoot,
@@ -95,7 +94,6 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
debugRenderPhaseSideEffectsForStrictMode,
disableLegacyContext,
disableModulePatternComponents,
enableProfilerCommitHooks,
enableProfilerTimer,
enableScopeAPI,
@@ -115,7 +113,12 @@ import shallowEqual from 'shared/shallowEqual';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
import getComponentNameFromType from 'shared/getComponentNameFromType';
import ReactStrictModeWarnings from './ReactStrictModeWarnings';
import {REACT_LAZY_TYPE, getIteratorFn} from 'shared/ReactSymbols';
import {
REACT_LAZY_TYPE,
REACT_FORWARD_REF_TYPE,
REACT_MEMO_TYPE,
getIteratorFn,
} from 'shared/ReactSymbols';
import {
getCurrentFiberOwnerNameInDevOrNull,
setIsRendering,
@@ -236,7 +239,6 @@ import {
queueHydrationError,
} from './ReactFiberHydrationContext';
import {
adoptClassInstance,
constructClassInstance,
mountClassInstance,
resumeMountClassInstance,
@@ -244,12 +246,12 @@ import {
} from './ReactFiberClassComponent';
import {resolveDefaultProps} from './ReactFiberLazyComponent';
import {
resolveLazyComponentTag,
createFiberFromTypeAndProps,
createFiberFromFragment,
createFiberFromOffscreen,
createWorkInProgress,
isSimpleFunctionComponent,
isFunctionClassComponent,
} from './ReactFiber';
import {
retryDehydratedSuspenseBoundary,
@@ -305,7 +307,6 @@ export const SelectiveHydrationException: mixed = new Error(
let didReceiveUpdate: boolean = false;
let didWarnAboutBadClass;
let didWarnAboutModulePatternComponent;
let didWarnAboutContextTypeOnFunctionComponent;
let didWarnAboutGetDerivedStateOnFunctionComponent;
let didWarnAboutFunctionRefs;
@@ -316,7 +317,6 @@ let didWarnAboutDefaultPropsOnFunctionComponent;
if (__DEV__) {
didWarnAboutBadClass = ({}: {[string]: boolean});
didWarnAboutModulePatternComponent = ({}: {[string]: boolean});
didWarnAboutContextTypeOnFunctionComponent = ({}: {[string]: boolean});
didWarnAboutGetDerivedStateOnFunctionComponent = ({}: {[string]: boolean});
didWarnAboutFunctionRefs = ({}: {[string]: boolean});
@@ -1053,6 +1053,43 @@ function updateFunctionComponent(
nextProps: any,
renderLanes: Lanes,
) {
if (__DEV__) {
if (
Component.prototype &&
typeof Component.prototype.render === 'function'
) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutBadClass[componentName]) {
console.error(
"The <%s /> component appears to have a render method, but doesn't extend React.Component. " +
'This is likely to cause errors. Change %s to extend React.Component instead.',
componentName,
componentName,
);
didWarnAboutBadClass[componentName] = true;
}
}
if (workInProgress.mode & StrictLegacyMode) {
ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null);
}
if (current === null) {
// Some validations were previously done in mountIndeterminateComponent however and are now run
// in updateFuntionComponent but only on mount
validateFunctionComponentInDev(workInProgress, workInProgress.type);
if (disableLegacyContext && Component.contextTypes) {
console.error(
'%s uses the legacy contextTypes API which was removed in React 19. ' +
'Use React.createContext() with React.useContext() instead.',
getComponentNameFromType(Component) || 'Unknown',
);
}
}
}
let context;
if (!disableLegacyContext) {
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
@@ -1699,64 +1736,64 @@ function mountLazyComponent(
let Component = init(payload);
// Store the unwrapped component in the type.
workInProgress.type = Component;
const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component));
const resolvedProps = resolveDefaultProps(Component, props);
let child;
switch (resolvedTag) {
case FunctionComponent: {
if (typeof Component === 'function') {
if (isFunctionClassComponent(Component)) {
workInProgress.tag = ClassComponent;
if (__DEV__) {
workInProgress.type = Component =
resolveClassForHotReloading(Component);
}
return updateClassComponent(
null,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
} else {
workInProgress.tag = FunctionComponent;
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, Component);
workInProgress.type = Component =
resolveFunctionForHotReloading(Component);
}
child = updateFunctionComponent(
return updateFunctionComponent(
null,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
return child;
}
case ClassComponent: {
if (__DEV__) {
workInProgress.type = Component =
resolveClassForHotReloading(Component);
}
child = updateClassComponent(
null,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
return child;
}
case ForwardRef: {
} else if (Component !== undefined && Component !== null) {
const $$typeof = Component.$$typeof;
if ($$typeof === REACT_FORWARD_REF_TYPE) {
workInProgress.tag = ForwardRef;
if (__DEV__) {
workInProgress.type = Component =
resolveForwardRefForHotReloading(Component);
}
child = updateForwardRef(
return updateForwardRef(
null,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
return child;
}
case MemoComponent: {
child = updateMemoComponent(
} else if ($$typeof === REACT_MEMO_TYPE) {
workInProgress.tag = MemoComponent;
return updateMemoComponent(
null,
workInProgress,
Component,
resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too
renderLanes,
);
return child;
}
}
let hint = '';
if (__DEV__) {
if (
@@ -1816,194 +1853,6 @@ function mountIncompleteClassComponent(
);
}
function mountIndeterminateComponent(
_current: null | Fiber,
workInProgress: Fiber,
Component: $FlowFixMe,
renderLanes: Lanes,
) {
resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress);
const props = workInProgress.pendingProps;
let context;
if (!disableLegacyContext) {
const unmaskedContext = getUnmaskedContext(
workInProgress,
Component,
false,
);
context = getMaskedContext(workInProgress, unmaskedContext);
}
prepareToReadContext(workInProgress, renderLanes);
let value;
let hasId;
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
if (__DEV__) {
if (
Component.prototype &&
typeof Component.prototype.render === 'function'
) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutBadClass[componentName]) {
console.error(
"The <%s /> component appears to have a render method, but doesn't extend React.Component. " +
'This is likely to cause errors. Change %s to extend React.Component instead.',
componentName,
componentName,
);
didWarnAboutBadClass[componentName] = true;
}
}
if (workInProgress.mode & StrictLegacyMode) {
ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null);
}
setIsRendering(true);
ReactCurrentOwner.current = workInProgress;
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
setIsRendering(false);
} else {
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
if (__DEV__) {
// Support for module components is deprecated and is removed behind a flag.
// Whether or not it would crash later, we want to show a good message in DEV first.
if (
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutModulePatternComponent[componentName]) {
console.error(
'The <%s /> component appears to be a function component that returns a class instance. ' +
'Change %s to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
"`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " +
'cannot be called with `new` by React.',
componentName,
componentName,
componentName,
);
didWarnAboutModulePatternComponent[componentName] = true;
}
}
}
if (
// Run these checks in production only if the flag is off.
// Eventually we'll delete this branch altogether.
!disableModulePatternComponents &&
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
if (__DEV__) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutModulePatternComponent[componentName]) {
console.error(
'The <%s /> component appears to be a function component that returns a class instance. ' +
'Change %s to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
"`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " +
'cannot be called with `new` by React.',
componentName,
componentName,
componentName,
);
didWarnAboutModulePatternComponent[componentName] = true;
}
}
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;
// Throw out any hooks that were used.
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
// Push context providers early to prevent context stack mismatches.
// During mounting we don't know the child context yet as the instance doesn't exist.
// We will invalidate the child context in finishClassComponent() right after rendering.
let hasContext = false;
if (isLegacyContextProvider(Component)) {
hasContext = true;
pushLegacyContextProvider(workInProgress);
} else {
hasContext = false;
}
workInProgress.memoizedState =
value.state !== null && value.state !== undefined ? value.state : null;
initializeUpdateQueue(workInProgress);
adoptClassInstance(workInProgress, value);
mountClassInstance(workInProgress, Component, props, renderLanes);
return finishClassComponent(
null,
workInProgress,
Component,
true,
hasContext,
renderLanes,
);
} else {
// Proceed under the assumption that this is a function component
workInProgress.tag = FunctionComponent;
if (__DEV__) {
if (disableLegacyContext && Component.contextTypes) {
console.error(
'%s uses the legacy contextTypes API which was removed in React 19. ' +
'Use React.createContext() with React.useContext() instead.',
getComponentNameFromType(Component) || 'Unknown',
);
}
}
if (getIsHydrating() && hasId) {
pushMaterializedTreeId(workInProgress);
}
reconcileChildren(null, workInProgress, value, renderLanes);
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, Component);
}
return workInProgress.child;
}
}
function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
if (__DEV__) {
if (Component) {
@@ -4027,14 +3876,6 @@ function beginWork(
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
+7 -12
View File
@@ -569,16 +569,6 @@ function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) {
}
}
function adoptClassInstance(workInProgress: Fiber, instance: any): void {
instance.updater = classComponentUpdater;
workInProgress.stateNode = instance;
// The instance needs access to the fiber so that it can schedule updates
setInstance(instance, workInProgress);
if (__DEV__) {
instance._reactInternalInstance = fakeInternalInstance;
}
}
function constructClassInstance(
workInProgress: Fiber,
ctor: any,
@@ -659,7 +649,13 @@ function constructClassInstance(
instance.state !== null && instance.state !== undefined
? instance.state
: null);
adoptClassInstance(workInProgress, instance);
instance.updater = classComponentUpdater;
workInProgress.stateNode = instance;
// The instance needs access to the fiber so that it can schedule updates
setInstance(instance, workInProgress);
if (__DEV__) {
instance._reactInternalInstance = fakeInternalInstance;
}
if (__DEV__) {
if (typeof ctor.getDerivedStateFromProps === 'function' && state === null) {
@@ -1230,7 +1226,6 @@ function updateClassInstance(
}
export {
adoptClassInstance,
constructClassInstance,
mountClassInstance,
resumeMountClassInstance,
@@ -45,7 +45,6 @@ import {
import {now} from './Scheduler';
import {
IndeterminateComponent,
FunctionComponent,
ClassComponent,
HostRoot,
@@ -949,7 +948,6 @@ function completeWork(
// for hydration.
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
@@ -17,7 +17,6 @@ import {
SuspenseComponent,
SuspenseListComponent,
FunctionComponent,
IndeterminateComponent,
ForwardRef,
SimpleMemoComponent,
ClassComponent,
@@ -47,7 +46,6 @@ function describeFiber(fiber: Fiber): string {
case SuspenseListComponent:
return describeBuiltInComponentFrame('SuspenseList', owner);
case FunctionComponent:
case IndeterminateComponent:
case SimpleMemoComponent:
return describeFunctionComponentFrame(fiber.type, owner);
case ForwardRef:
@@ -17,7 +17,6 @@ import {
SuspenseComponent,
SuspenseListComponent,
FunctionComponent,
IndeterminateComponent,
ForwardRef,
SimpleMemoComponent,
ClassComponent,
@@ -87,7 +86,6 @@ function describeFiberType(fiber: Fiber): null | string {
case SuspenseListComponent:
return 'SuspenseList';
case FunctionComponent:
case IndeterminateComponent:
case SimpleMemoComponent:
const fn = fiber.type;
return fn.displayName || fn.name || null;
-8
View File
@@ -90,7 +90,6 @@ import {
} from './ReactTypeOfMode';
import {
HostRoot,
IndeterminateComponent,
ClassComponent,
SuspenseComponent,
SuspenseListComponent,
@@ -2395,12 +2394,6 @@ function replaySuspendedUnitOfWork(unitOfWork: Fiber): void {
startProfilerTimer(unitOfWork);
}
switch (unitOfWork.tag) {
case IndeterminateComponent: {
// Because it suspended with `use`, we can assume it's a
// function component.
unitOfWork.tag = FunctionComponent;
// Fallthrough to the next branch.
}
case SimpleMemoComponent:
case FunctionComponent: {
// Resolve `defaultProps`. This logic is copied from `beginWork`.
@@ -3823,7 +3816,6 @@ export function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber: Fiber) {
const tag = fiber.tag;
if (
tag !== IndeterminateComponent &&
tag !== HostRoot &&
tag !== ClassComponent &&
tag !== FunctionComponent &&
-1
View File
@@ -39,7 +39,6 @@ export type WorkTag =
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
@@ -1308,16 +1308,6 @@ describe('ReactHooks', () => {
return <div />;
});
function Factory() {
return {
state: {},
render() {
renderCount++;
return <div />;
},
};
}
let renderer;
await act(() => {
renderer = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
@@ -1410,46 +1400,6 @@ describe('ReactHooks', () => {
});
expect(renderCount).toBe(__DEV__ ? 2 : 1);
if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
renderCount = 0;
await expect(async () => {
await act(() => {
renderer.update(<Factory />);
});
}).toErrorDev(
'Warning: The <Factory /> component appears to be a function component that returns a class instance. ' +
'Change Factory to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
'`Factory.prototype = React.Component.prototype`. ' +
"Don't use an arrow function since it cannot be called with `new` by React.",
);
expect(renderCount).toBe(1);
renderCount = 0;
await act(() => {
renderer.update(<Factory />);
});
expect(renderCount).toBe(1);
renderCount = 0;
await act(() => {
renderer.update(
<StrictMode>
<Factory />
</StrictMode>,
);
});
expect(renderCount).toBe(__DEV__ ? 2 : 1); // Treated like a class
renderCount = 0;
await act(() => {
renderer.update(
<StrictMode>
<Factory />
</StrictMode>,
);
});
expect(renderCount).toBe(__DEV__ ? 2 : 1); // Treated like a class
}
renderCount = 0;
await act(() => {
renderer.update(<HasHooks />);
@@ -227,44 +227,6 @@ describe('ReactHooksWithNoopRenderer', () => {
await waitForAll([10]);
});
// @gate !disableModulePatternComponents
it('throws inside module-style components', async () => {
function Counter() {
return {
render() {
const [count] = useState(0);
return <Text text={this.props.label + ': ' + count} />;
},
};
}
ReactNoop.render(<Counter />);
await expect(
async () =>
await waitForThrow(
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen ' +
'for one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',
),
).toErrorDev(
'Warning: The <Counter /> component appears to be a function component that returns a class instance. ' +
'Change Counter to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
'`Counter.prototype = React.Component.prototype`. ' +
"Don't use an arrow function since it cannot be called with `new` by React.",
);
// Confirm that a subsequent hook works properly.
function GoodCounter(props) {
const [count] = useState(props.initialCount);
return <Text text={count} />;
}
ReactNoop.render(<GoodCounter initialCount={10} />);
await waitForAll([10]);
});
it('throws when called outside the render phase', async () => {
expect(() => {
expect(() => useState(0)).toThrow(
@@ -1864,48 +1864,6 @@ describe('ReactIncremental', () => {
]);
});
// @gate !disableModulePatternComponents
// @gate !disableLegacyContext
it('does not leak own context into context provider (factory components)', async () => {
function Recurse(props, context) {
return {
getChildContext() {
return {n: (context.n || 3) - 1};
},
render() {
Scheduler.log('Recurse ' + JSON.stringify(context));
if (context.n === 0) {
return null;
}
return <Recurse />;
},
};
}
Recurse.contextTypes = {
n: PropTypes.number,
};
Recurse.childContextTypes = {
n: PropTypes.number,
};
ReactNoop.render(<Recurse />);
await expect(
async () =>
await waitForAll([
'Recurse {}',
'Recurse {"n":2}',
'Recurse {"n":1}',
'Recurse {"n":0}',
]),
).toErrorDev([
'Warning: The <Recurse /> component appears to be a function component that returns a class instance. ' +
'Change Recurse to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
'`Recurse.prototype = React.Component.prototype`. ' +
"Don't use an arrow function since it cannot be called with `new` by React.",
]);
});
// @gate www
// @gate !disableLegacyContext
it('provides context when reusing work', async () => {
@@ -1754,45 +1754,6 @@ describe('ReactIncrementalErrorHandling', () => {
);
});
// @gate !disableModulePatternComponents
it('handles error thrown inside getDerivedStateFromProps of a module-style context provider', async () => {
function Provider() {
return {
getChildContext() {
return {foo: 'bar'};
},
render() {
return 'Hi';
},
};
}
Provider.childContextTypes = {
x: () => {},
};
Provider.getDerivedStateFromProps = () => {
throw new Error('Oops!');
};
ReactNoop.render(<Provider />);
await expect(async () => {
await waitForThrow('Oops!');
}).toErrorDev([
'Warning: The <Provider /> component appears to be a function component that returns a class instance. ' +
'Change Provider to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
'`Provider.prototype = React.Component.prototype`. ' +
"Don't use an arrow function since it cannot be called with `new` by React.",
...gate(flags =>
flags.disableLegacyContext
? [
'Warning: Provider uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.',
'Warning: Provider uses the legacy childContextTypes API which was removed in React 19. Use React.createContext() instead.',
]
: [],
),
]);
});
it('uncaught errors should be discarded if the render is aborted', async () => {
const root = ReactNoop.createRoot();
@@ -132,11 +132,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// @gate experimental || www
it('regression: false positive for legacy suspense', async () => {
// Wrapping in memo because regular function components go through the
// mountIndeterminateComponent path, which acts like there's no `current`
// fiber even though there is. `memo` is not indeterminate, so it goes
// through the update path.
const Child = React.memo(({text}) => {
const Child = ({text}) => {
// If text hasn't resolved, this will throw and exit before the passive
// static effect flag is added by the useEffect call below.
readText(text);
@@ -147,7 +143,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
Scheduler.log(text);
return text;
});
};
function App() {
return (
@@ -18,7 +18,6 @@ import {
import {
FunctionComponent,
ClassComponent,
IndeterminateComponent,
HostRoot,
HostPortal,
HostComponent,
@@ -128,7 +127,6 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null {
case ClassComponent:
case FunctionComponent:
case IncompleteClassComponent:
case IndeterminateComponent:
case MemoComponent:
case SimpleMemoComponent:
if (typeof type === 'function') {
@@ -1639,54 +1639,6 @@ describe('ReactFreshIntegration', () => {
}
});
if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
it('remounts deprecated factory components', async () => {
if (__DEV__) {
await expect(async () => {
await render(`
function Parent() {
return {
render() {
return <Child prop="A" />;
}
};
};
function Child({prop}) {
return <h1>{prop}1</h1>;
};
export default Parent;
`);
}).toErrorDev(
'The <Parent /> component appears to be a function component ' +
'that returns a class instance.',
);
const el = container.firstChild;
expect(el.textContent).toBe('A1');
await patch(`
function Parent() {
return {
render() {
return <Child prop="B" />;
}
};
};
function Child({prop}) {
return <h1>{prop}2</h1>;
};
export default Parent;
`);
// Like classes, factory components always remount.
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('B2');
}
});
}
describe('with inline requires', () => {
beforeEach(() => {
global.FakeModuleSystem = {};
+19 -80
View File
@@ -137,7 +137,6 @@ import {
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
disableLegacyContext,
disableModulePatternComponents,
enableBigIntSupport,
enableScopeAPI,
enableSuspenseAvoidThisFallbackFizz,
@@ -1388,7 +1387,6 @@ function renderClassComponent(
}
const didWarnAboutBadClass: {[string]: boolean} = {};
const didWarnAboutModulePatternComponent: {[string]: boolean} = {};
const didWarnAboutContextTypeOnFunctionComponent: {[string]: boolean} = {};
const didWarnAboutGetDerivedStateOnFunctionComponent: {[string]: boolean} = {};
let didWarnAboutReassigningProps = false;
@@ -1396,9 +1394,7 @@ const didWarnAboutDefaultPropsOnFunctionComponent: {[string]: boolean} = {};
let didWarnAboutGenerators = false;
let didWarnAboutMaps = false;
// This would typically be a function component but we still support module pattern
// components for some reason.
function renderIndeterminateComponent(
function renderFunctionComponent(
request: Request,
task: Task,
keyPath: KeyNode,
@@ -1444,83 +1440,26 @@ function renderIndeterminateComponent(
const actionStateMatchingIndex = getActionStateMatchingIndex();
if (__DEV__) {
// Support for module components is deprecated and is removed behind a flag.
// Whether or not it would crash later, we want to show a good message in DEV first.
if (
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutModulePatternComponent[componentName]) {
console.error(
'The <%s /> component appears to be a function component that returns a class instance. ' +
'Change %s to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
"`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " +
'cannot be called with `new` by React.',
componentName,
componentName,
componentName,
);
didWarnAboutModulePatternComponent[componentName] = true;
}
if (disableLegacyContext && Component.contextTypes) {
console.error(
'%s uses the legacy contextTypes API which was removed in React 19. ' +
'Use React.createContext() with React.useContext() instead.',
getComponentNameFromType(Component) || 'Unknown',
);
}
}
if (
// Run these checks in production only if the flag is off.
// Eventually we'll delete this branch altogether.
!disableModulePatternComponents &&
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
if (__DEV__) {
const componentName = getComponentNameFromType(Component) || 'Unknown';
if (!didWarnAboutModulePatternComponent[componentName]) {
console.error(
'The <%s /> component appears to be a function component that returns a class instance. ' +
'Change %s to a class that extends React.Component instead. ' +
"If you can't use a class try assigning the prototype on the function as a workaround. " +
"`%s.prototype = React.Component.prototype`. Don't use an arrow function since it " +
'cannot be called with `new` by React.',
componentName,
componentName,
componentName,
);
didWarnAboutModulePatternComponent[componentName] = true;
}
}
mountClassInstance(value, Component, props, legacyContext);
finishClassComponent(request, task, keyPath, value, Component, props);
} else {
// Proceed under the assumption that this is a function component
if (__DEV__) {
if (disableLegacyContext && Component.contextTypes) {
console.error(
'%s uses the legacy contextTypes API which was removed in React 19. ' +
'Use React.createContext() with React.useContext() instead.',
getComponentNameFromType(Component) || 'Unknown',
);
}
}
if (__DEV__) {
validateFunctionComponentInDev(Component);
}
finishFunctionComponent(
request,
task,
keyPath,
value,
hasId,
actionStateCount,
actionStateMatchingIndex,
);
if (__DEV__) {
validateFunctionComponentInDev(Component);
}
finishFunctionComponent(
request,
task,
keyPath,
value,
hasId,
actionStateCount,
actionStateMatchingIndex,
);
task.componentStack = previousComponentStack;
}
@@ -1825,7 +1764,7 @@ function renderElement(
renderClassComponent(request, task, keyPath, type, props);
return;
} else {
renderIndeterminateComponent(request, task, keyPath, type, props);
renderFunctionComponent(request, task, keyPath, type, props);
return;
}
}
-2
View File
@@ -206,8 +206,6 @@ export const enableRenderableContext = __NEXT_MAJOR__;
// when we plan to enable them.
// -----------------------------------------------------------------------------
export const disableModulePatternComponents = __NEXT_MAJOR__;
export const enableUseRefAccessWarning = false;
// Enables time slicing for updates that aren't wrapped in startTransition.
@@ -34,7 +34,6 @@ export const {
} = dynamicFlags;
// The rest of the flags are static for better dead code elimination.
export const disableModulePatternComponents = true;
export const enableDebugTracing = false;
export const enableAsyncDebugInfo = false;
export const enableSchedulingProfiler = __PROFILE__;
@@ -31,7 +31,6 @@ export const enableDeferRootSchedulingToMicrotask = __TODO_NEXT_RN_MAJOR__;
export const alwaysThrottleRetries = __TODO_NEXT_RN_MAJOR__;
export const enableInfiniteRenderLoopDetection = __TODO_NEXT_RN_MAJOR__;
export const enableComponentStackLocations = __TODO_NEXT_RN_MAJOR__;
export const disableModulePatternComponents = __TODO_NEXT_RN_MAJOR__;
// -----------------------------------------------------------------------------
// These are ready to flip after the next React npm release (or RN switches to
@@ -94,7 +94,6 @@ export const disableLegacyMode = __NEXT_MAJOR__;
export const disableLegacyContext = __NEXT_MAJOR__;
export const disableDOMTestUtils = __NEXT_MAJOR__;
export const enableNewBooleanProps = __NEXT_MAJOR__;
export const disableModulePatternComponents = __NEXT_MAJOR__;
export const enableRenderableContext = __NEXT_MAJOR__;
export const enableReactTestRendererWarning = __NEXT_MAJOR__;
@@ -34,7 +34,6 @@ export const enableSuspenseCallback = false;
export const disableLegacyContext = false;
export const enableTrustedTypesIntegration = false;
export const disableTextareaChildren = false;
export const disableModulePatternComponents = true;
export const enableComponentStackLocations = false;
export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = true;
@@ -34,7 +34,6 @@ export const enableSuspenseCallback = true;
export const disableLegacyContext = false;
export const enableTrustedTypesIntegration = false;
export const disableTextareaChildren = false;
export const disableModulePatternComponents = true;
export const enableSuspenseAvoidThisFallback = true;
export const enableSuspenseAvoidThisFallbackFizz = false;
export const enableCPUSuspense = false;
@@ -82,8 +82,6 @@ export const enablePostpone = false;
// Need to remove it.
export const disableCommentsAsDOMContainers = false;
export const disableModulePatternComponents = true;
export const enableCreateEventHandleAPI = true;
export const enableScopeAPI = true;