Remove non-JSX propTypes checks (#28326)

Removes all `propTypes` validation called from outside the JSX
factories. Haven't touched JSX.

Tests that verify related behavior are stripped down to the
non-`propTypes` logic.
This commit is contained in:
dan
2024-02-16 00:35:22 +00:00
committed by GitHub
parent 92686722ca
commit fea900e454
6 changed files with 56 additions and 375 deletions

View File

@@ -40,7 +40,6 @@ import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
import type {TransitionStatus} from './ReactFiberConfig';
import type {Hook} from './ReactFiberHooks';
import checkPropTypes from 'shared/checkPropTypes';
import {
markComponentRenderStarted,
markComponentRenderStopped,
@@ -401,23 +400,6 @@ function updateForwardRef(
// TODO: current can be non-null here even if the component
// hasn't yet mounted. This happens after the first render suspends.
// We'll need to figure out if this is fine or can cause issues.
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
// Lazy component props can't be validated in createElement
// because they're only guaranteed to be resolved here.
const innerPropTypes = Component.propTypes;
if (innerPropTypes) {
checkPropTypes(
innerPropTypes,
nextProps, // Resolved props
'prop',
getComponentNameFromType(Component),
);
}
}
}
const render = Component.render;
const ref = workInProgress.ref;
@@ -507,17 +489,6 @@ function updateMemoComponent(
);
}
if (__DEV__) {
const innerPropTypes = type.propTypes;
if (innerPropTypes) {
// Inner memo component props aren't currently validated in createElement.
// We could move it there, but we'd still need this for lazy code path.
checkPropTypes(
innerPropTypes,
nextProps, // Resolved props
'prop',
getComponentNameFromType(type),
);
}
if (Component.defaultProps !== undefined) {
const componentName = getComponentNameFromType(type) || 'Unknown';
if (!didWarnAboutDefaultPropsOnFunctionComponent[componentName]) {
@@ -543,20 +514,6 @@ function updateMemoComponent(
workInProgress.child = child;
return child;
}
if (__DEV__) {
const type = Component.type;
const innerPropTypes = type.propTypes;
if (innerPropTypes) {
// Inner memo component props aren't currently validated in createElement.
// We could move it there, but we'd still need this for lazy code path.
checkPropTypes(
innerPropTypes,
nextProps, // Resolved props
'prop',
getComponentNameFromType(type),
);
}
}
const currentChild = ((current.child: any): Fiber); // This is always exactly one child
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
@@ -592,37 +549,6 @@ function updateSimpleMemoComponent(
// TODO: current can be non-null here even if the component
// hasn't yet mounted. This happens when the inner render suspends.
// We'll need to figure out if this is fine or can cause issues.
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
// Lazy component props can't be validated in createElement
// because they're only guaranteed to be resolved here.
let outerMemoType = workInProgress.elementType;
if (outerMemoType.$$typeof === REACT_LAZY_TYPE) {
// We warn when you define propTypes on lazy()
// so let's just skip over it to find memo() outer wrapper.
// Inner props for memo are validated later.
const lazyComponent: LazyComponentType<any, any> = outerMemoType;
const payload = lazyComponent._payload;
const init = lazyComponent._init;
try {
outerMemoType = init(payload);
} catch (x) {
outerMemoType = null;
}
// Inner propTypes will be validated in the function component path.
const outerPropTypes = outerMemoType && (outerMemoType: any).propTypes;
if (outerPropTypes) {
checkPropTypes(
outerPropTypes,
nextProps, // Resolved (SimpleMemoComponent has no defaultProps)
'prop',
getComponentNameFromType(outerMemoType),
);
}
}
}
}
if (current !== null) {
const prevProps = current.memoizedProps;
if (
@@ -1099,22 +1025,6 @@ function updateFunctionComponent(
nextProps: any,
renderLanes: Lanes,
) {
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
// Lazy component props can't be validated in createElement
// because they're only guaranteed to be resolved here.
const innerPropTypes = Component.propTypes;
if (innerPropTypes) {
checkPropTypes(
innerPropTypes,
nextProps, // Resolved props
'prop',
getComponentNameFromType(Component),
);
}
}
}
let context;
if (!disableLegacyContext) {
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
@@ -1253,20 +1163,6 @@ function updateClassComponent(
break;
}
}
if (workInProgress.type !== workInProgress.elementType) {
// Lazy component props can't be validated in createElement
// because they're only guaranteed to be resolved here.
const innerPropTypes = Component.propTypes;
if (innerPropTypes) {
checkPropTypes(
innerPropTypes,
nextProps, // Resolved props
'prop',
getComponentNameFromType(Component),
);
}
}
}
// Push context providers early to prevent context stack mismatches.
@@ -1815,19 +1711,6 @@ function mountLazyComponent(
return child;
}
case MemoComponent: {
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
const outerPropTypes = Component.propTypes;
if (outerPropTypes) {
checkPropTypes(
outerPropTypes,
resolvedProps, // Resolved for outer only
'prop',
getComponentNameFromType(Component),
);
}
}
}
child = updateMemoComponent(
null,
workInProgress,
@@ -3549,11 +3432,6 @@ function updateContextProvider(
);
}
}
const providerPropTypes = workInProgress.type.propTypes;
if (providerPropTypes) {
checkPropTypes(providerPropTypes, newProps, 'prop', 'Context.Provider');
}
}
pushProvider(workInProgress, context, newValue);
@@ -4229,19 +4107,6 @@ function beginWork(
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
const outerPropTypes = type.propTypes;
if (outerPropTypes) {
checkPropTypes(
outerPropTypes,
resolvedProps, // Resolved for outer only
'prop',
getComponentNameFromType(type),
);
}
}
}
resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
current,

View File

@@ -1,4 +1,3 @@
let PropTypes;
let React;
let ReactTestRenderer;
let Scheduler;
@@ -28,7 +27,6 @@ describe('ReactLazy', () => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
PropTypes = require('prop-types');
React = require('react');
Suspense = React.Suspense;
lazy = React.lazy;
@@ -783,33 +781,12 @@ describe('ReactLazy', () => {
);
});
it('warns about defining propTypes on the outer wrapper', () => {
const LazyText = lazy(() => fakeImport(Text));
expect(() => {
LazyText.propTypes = {hello: () => {}};
}).toErrorDev(
'React.lazy(...): It is not supported to assign `propTypes` to ' +
'a lazy component import. Either specify them where the component ' +
'is defined, or create a wrapping component around it.',
{withoutStack: true},
);
});
async function verifyInnerPropTypesAreChecked(
async function verifyResolvesProps(
Add,
shouldWarnAboutFunctionDefaultProps,
shouldWarnAboutMemoDefaultProps,
) {
const LazyAdd = lazy(() => fakeImport(Add));
expect(() => {
LazyAdd.propTypes = {};
}).toErrorDev(
'React.lazy(...): It is not supported to assign `propTypes` to ' +
'a lazy component import. Either specify them where the component ' +
'is defined, or create a wrapping component around it.',
{withoutStack: true},
);
const root = ReactTestRenderer.create(
<Suspense fallback={<Text text="Loading..." />}>
<LazyAdd inner="2" outer="2" />
@@ -820,7 +797,6 @@ describe('ReactLazy', () => {
);
await waitForAll(['Loading...']);
expect(root).not.toMatchRenderedOutput('22');
// Mount
@@ -830,171 +806,126 @@ describe('ReactLazy', () => {
shouldWarnAboutFunctionDefaultProps
? [
'Add: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.',
'Invalid prop `inner` of type `string` supplied to `Add`, expected `number`.',
]
: shouldWarnAboutMemoDefaultProps
? [
'Add: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.',
'Invalid prop `inner` of type `string` supplied to `Add`, expected `number`.',
]
: [
'Invalid prop `inner` of type `string` supplied to `Add`, expected `number`.',
],
: [],
);
expect(root).toMatchRenderedOutput('22');
// Update
await expect(async () => {
root.update(
<Suspense fallback={<Text text="Loading..." />}>
<LazyAdd inner={false} outer={false} />
</Suspense>,
);
await waitForAll([]);
}).toErrorDev(
'Invalid prop `inner` of type `boolean` supplied to `Add`, expected `number`.',
root.update(
<Suspense fallback={<Text text="Loading..." />}>
<LazyAdd inner={false} outer={false} />
</Suspense>,
);
await waitForAll([]);
expect(root).toMatchRenderedOutput('0');
}
// Note: all "with defaultProps" tests below also verify defaultProps works as expected.
// If we ever delete or move propTypes-related tests, make sure not to delete these.
it('respects propTypes on function component with defaultProps', async () => {
it('resolves props for function component with defaultProps', async () => {
function Add(props) {
expect(props.innerWithDefault).toBe(42);
return props.inner + props.outer;
}
Add.propTypes = {
inner: PropTypes.number.isRequired,
innerWithDefault: PropTypes.number.isRequired,
};
Add.defaultProps = {
innerWithDefault: 42,
};
await verifyInnerPropTypesAreChecked(Add, true);
await verifyResolvesProps(Add, true);
});
it('respects propTypes on function component without defaultProps', async () => {
it('resolves props for function component without defaultProps', async () => {
function Add(props) {
return props.inner + props.outer;
}
Add.propTypes = {
inner: PropTypes.number.isRequired,
};
await verifyInnerPropTypesAreChecked(Add);
await verifyResolvesProps(Add);
});
it('respects propTypes on class component with defaultProps', async () => {
it('resolves props for class component with defaultProps', async () => {
class Add extends React.Component {
render() {
expect(this.props.innerWithDefault).toBe(42);
return this.props.inner + this.props.outer;
}
}
Add.propTypes = {
inner: PropTypes.number.isRequired,
innerWithDefault: PropTypes.number.isRequired,
};
Add.defaultProps = {
innerWithDefault: 42,
};
await verifyInnerPropTypesAreChecked(Add);
await verifyResolvesProps(Add);
});
it('respects propTypes on class component without defaultProps', async () => {
it('resolves props for class component without defaultProps', async () => {
class Add extends React.Component {
render() {
return this.props.inner + this.props.outer;
}
}
Add.propTypes = {
inner: PropTypes.number.isRequired,
};
await verifyInnerPropTypesAreChecked(Add);
await verifyResolvesProps(Add);
});
it('respects propTypes on forwardRef component with defaultProps', async () => {
it('resolves props for forwardRef component with defaultProps', async () => {
const Add = React.forwardRef((props, ref) => {
expect(props.innerWithDefault).toBe(42);
return props.inner + props.outer;
});
Add.displayName = 'Add';
Add.propTypes = {
inner: PropTypes.number.isRequired,
innerWithDefault: PropTypes.number.isRequired,
};
Add.defaultProps = {
innerWithDefault: 42,
};
await verifyInnerPropTypesAreChecked(Add);
await verifyResolvesProps(Add);
});
it('respects propTypes on forwardRef component without defaultProps', async () => {
it('resolves props for forwardRef component without defaultProps', async () => {
const Add = React.forwardRef((props, ref) => {
return props.inner + props.outer;
});
Add.displayName = 'Add';
Add.propTypes = {
inner: PropTypes.number.isRequired,
};
await verifyInnerPropTypesAreChecked(Add);
await verifyResolvesProps(Add);
});
it('respects propTypes on outer memo component with defaultProps', async () => {
it('resolves props for outer memo component with defaultProps', async () => {
let Add = props => {
expect(props.innerWithDefault).toBe(42);
return props.inner + props.outer;
};
Add = React.memo(Add);
Add.propTypes = {
inner: PropTypes.number.isRequired,
innerWithDefault: PropTypes.number.isRequired,
};
Add.defaultProps = {
innerWithDefault: 42,
};
await verifyInnerPropTypesAreChecked(Add, false, true);
await verifyResolvesProps(Add, false, true);
});
it('respects propTypes on outer memo component without defaultProps', async () => {
it('resolves props for outer memo component without defaultProps', async () => {
let Add = props => {
return props.inner + props.outer;
};
Add = React.memo(Add);
Add.propTypes = {
inner: PropTypes.number.isRequired,
};
await verifyInnerPropTypesAreChecked(Add);
await verifyResolvesProps(Add);
});
it('respects propTypes on inner memo component with defaultProps', async () => {
it('resolves props for inner memo component with defaultProps', async () => {
const Add = props => {
expect(props.innerWithDefault).toBe(42);
return props.inner + props.outer;
};
Add.displayName = 'Add';
Add.propTypes = {
inner: PropTypes.number.isRequired,
innerWithDefault: PropTypes.number.isRequired,
};
Add.defaultProps = {
innerWithDefault: 42,
};
await verifyInnerPropTypesAreChecked(React.memo(Add), true);
await verifyResolvesProps(React.memo(Add), true);
});
it('respects propTypes on inner memo component without defaultProps', async () => {
it('resolves props for inner memo component without defaultProps', async () => {
const Add = props => {
return props.inner + props.outer;
};
Add.displayName = 'Add';
Add.propTypes = {
inner: PropTypes.number.isRequired,
};
await verifyInnerPropTypesAreChecked(React.memo(Add));
await verifyResolvesProps(React.memo(Add));
});
it('uses outer resolved props for validating propTypes on memo', async () => {
it('uses outer resolved props on memo', async () => {
let T = props => {
return <Text text={props.text} />;
};
@@ -1002,10 +933,6 @@ describe('ReactLazy', () => {
text: 'Inner default text',
};
T = React.memo(T);
T.propTypes = {
// Should not be satisfied by the *inner* defaultProps.
text: PropTypes.string.isRequired,
};
const LazyText = lazy(() => fakeImport(T));
const root = ReactTestRenderer.create(
<Suspense fallback={<Text text="Loading..." />}>
@@ -1025,21 +952,16 @@ describe('ReactLazy', () => {
assertLog(['Inner default text']);
}).toErrorDev([
'T: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.',
'The prop `text` is marked as required in `T`, but its value is `undefined`',
]);
expect(root).toMatchRenderedOutput('Inner default text');
// Update
await expect(async () => {
root.update(
<Suspense fallback={<Text text="Loading..." />}>
<LazyText text={null} />
</Suspense>,
);
await waitForAll([null]);
}).toErrorDev(
'The prop `text` is marked as required in `T`, but its value is `null`',
root.update(
<Suspense fallback={<Text text="Loading..." />}>
<LazyText text={null} />
</Suspense>,
);
await waitForAll([null]);
expect(root).toMatchRenderedOutput(null);
});

View File

@@ -12,7 +12,6 @@
'use strict';
let PropTypes;
let React;
let ReactNoop;
let Suspense;
@@ -25,7 +24,6 @@ describe('memo', () => {
beforeEach(() => {
jest.resetModules();
PropTypes = require('prop-types');
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
@@ -483,108 +481,39 @@ describe('memo', () => {
);
});
it('validates propTypes declared on the inner component', async () => {
function FnInner(props) {
return props.inner;
}
FnInner.propTypes = {inner: PropTypes.number.isRequired};
const Fn = React.memo(FnInner);
// Mount
await expect(async () => {
ReactNoop.render(<Fn inner="2" />);
await waitForAll([]);
}).toErrorDev(
'Invalid prop `inner` of type `string` supplied to `FnInner`, expected `number`.',
);
// Update
await expect(async () => {
ReactNoop.render(<Fn inner={false} />);
await waitForAll([]);
}).toErrorDev(
'Invalid prop `inner` of type `boolean` supplied to `FnInner`, expected `number`.',
);
});
it('validates propTypes declared on the outer component', async () => {
function FnInner(props) {
return props.outer;
}
const Fn = React.memo(FnInner);
Fn.propTypes = {outer: PropTypes.number.isRequired};
// Mount
await expect(async () => {
ReactNoop.render(<Fn outer="3" />);
await waitForAll([]);
}).toErrorDev(
// Outer props are checked in createElement
'Invalid prop `outer` of type `string` supplied to `FnInner`, expected `number`.',
);
// Update
await expect(async () => {
ReactNoop.render(<Fn outer={false} />);
await waitForAll([]);
}).toErrorDev(
// Outer props are checked in createElement
'Invalid prop `outer` of type `boolean` supplied to `FnInner`, expected `number`.',
);
});
it('validates nested propTypes declarations', async () => {
it('handles nested defaultProps declarations', async () => {
function Inner(props) {
return props.inner + props.middle + props.outer;
}
Inner.propTypes = {inner: PropTypes.number.isRequired};
Inner.defaultProps = {inner: 0};
Inner.defaultProps = {inner: 1};
const Middle = React.memo(Inner);
Middle.propTypes = {middle: PropTypes.number.isRequired};
Middle.defaultProps = {middle: 0};
Middle.defaultProps = {middle: 10};
const Outer = React.memo(Middle);
Outer.propTypes = {outer: PropTypes.number.isRequired};
Outer.defaultProps = {outer: 0};
Outer.defaultProps = {outer: 100};
// No warning expected because defaultProps satisfy both.
ReactNoop.render(
<div>
<Outer />
</div>,
);
const root = ReactNoop.createRoot();
await expect(async () => {
await waitForAll([]);
await act(() => {
root.render(
<div>
<Outer />
</div>,
);
});
}).toErrorDev([
'Inner: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.',
'Support for defaultProps will be removed from memo component',
]);
expect(root).toMatchRenderedOutput(<div>111</div>);
// Mount
await expect(async () => {
ReactNoop.render(
await act(async () => {
root.render(
<div>
<Outer inner="2" middle="3" outer="4" />
</div>,
);
await waitForAll([]);
}).toErrorDev([
'Invalid prop `outer` of type `string` supplied to `Inner`, expected `number`.',
'Invalid prop `middle` of type `string` supplied to `Inner`, expected `number`.',
'Invalid prop `inner` of type `string` supplied to `Inner`, expected `number`.',
]);
// Update
await expect(async () => {
ReactNoop.render(
<div>
<Outer inner={false} middle={false} outer={false} />
</div>,
);
await waitForAll([]);
}).toErrorDev([
'Invalid prop `outer` of type `boolean` supplied to `Inner`, expected `number`.',
'Invalid prop `middle` of type `boolean` supplied to `Inner`, expected `number`.',
'Invalid prop `inner` of type `boolean` supplied to `Inner`, expected `number`.',
]);
});
expect(root).toMatchRenderedOutput(<div>234</div>);
});
it('does not drop lower priority state updates when bailing out at higher pri (simple)', async () => {

View File

@@ -9,7 +9,6 @@
import {disableLegacyContext} from 'shared/ReactFeatureFlags';
import getComponentNameFromType from 'shared/getComponentNameFromType';
import checkPropTypes from 'shared/checkPropTypes';
let warnedAboutMissingGetChildContext;
@@ -36,11 +35,6 @@ export function getMaskedContext(type: any, unmaskedContext: Object): Object {
context[key] = unmaskedContext[key];
}
if (__DEV__) {
const name = getComponentNameFromType(type) || 'Unknown';
checkPropTypes(contextTypes, context, 'context', name);
}
return context;
}
}
@@ -84,11 +78,6 @@ export function processChildContext(
);
}
}
if (__DEV__) {
const name = getComponentNameFromType(type) || 'Unknown';
checkPropTypes(childContextTypes, childContext, 'child context', name);
}
return {...parentContext, ...childContext};
}
}

View File

@@ -36,9 +36,9 @@ export function forwardRef<Props, ElementType: React$ElementType>(
}
if (render != null) {
if (render.defaultProps != null || render.propTypes != null) {
if (render.defaultProps != null) {
console.error(
'forwardRef render functions do not support propTypes or defaultProps. ' +
'forwardRef render functions do not support defaultProps. ' +
'Did you accidentally pass a React component?',
);
}

View File

@@ -10,14 +10,12 @@
'use strict';
describe('forwardRef', () => {
let PropTypes;
let React;
let ReactNoop;
let waitForAll;
beforeEach(() => {
jest.resetModules();
PropTypes = require('prop-types');
React = require('react');
ReactNoop = require('react-noop-renderer');
@@ -76,7 +74,7 @@ describe('forwardRef', () => {
expect(ref.current).toBe(null);
});
it('should support propTypes and defaultProps', async () => {
it('should support defaultProps', async () => {
function FunctionComponent({forwardedRef, optional, required}) {
return (
<div ref={forwardedRef}>
@@ -91,10 +89,6 @@ describe('forwardRef', () => {
return <FunctionComponent {...props} forwardedRef={ref} />;
},
);
RefForwardingComponent.propTypes = {
optional: PropTypes.string,
required: PropTypes.string.isRequired,
};
RefForwardingComponent.defaultProps = {
optional: 'default',
};
@@ -116,14 +110,6 @@ describe('forwardRef', () => {
{text: 'default', hidden: false},
{text: 'foo', hidden: false},
]);
expect(() =>
ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />),
).toErrorDev(
'Warning: Failed prop type: The prop `required` is marked as required in ' +
'`ForwardRef(NamedFunction)`, but its value is `undefined`.\n' +
' in NamedFunction (at **)',
);
});
it('should warn if not provided a callback during creation', () => {
@@ -150,24 +136,14 @@ describe('forwardRef', () => {
);
});
it('should warn if the render function provided has propTypes or defaultProps attributes', () => {
function renderWithPropTypes(props, ref) {
return null;
}
renderWithPropTypes.propTypes = {};
it('should warn if the render function provided has defaultProps attributes', () => {
function renderWithDefaultProps(props, ref) {
return null;
}
renderWithDefaultProps.defaultProps = {};
expect(() => React.forwardRef(renderWithPropTypes)).toErrorDev(
'forwardRef render functions do not support propTypes or defaultProps. ' +
'Did you accidentally pass a React component?',
{withoutStack: true},
);
expect(() => React.forwardRef(renderWithDefaultProps)).toErrorDev(
'forwardRef render functions do not support propTypes or defaultProps. ' +
'forwardRef render functions do not support defaultProps. ' +
'Did you accidentally pass a React component?',
{withoutStack: true},
);