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