diff --git a/packages/react-reconciler/src/__tests__/ReactMemo-test.internal.js b/packages/react-reconciler/src/__tests__/ReactMemo-test.internal.js index 897a5276d7..2a71174be6 100644 --- a/packages/react-reconciler/src/__tests__/ReactMemo-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactMemo-test.internal.js @@ -12,6 +12,7 @@ 'use strict'; +let PropTypes; let React; let ReactFeatureFlags; let ReactNoop; @@ -22,6 +23,7 @@ describe('memo', () => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + PropTypes = require('prop-types'); React = require('react'); ReactNoop = require('react-noop-renderer'); ({Suspense} = React); @@ -321,6 +323,58 @@ describe('memo', () => { {withoutStack: true}, ); }); + + it('validates propTypes declared on the inner component', () => { + function FnInner(props) { + return props.inner + props.outer; + } + FnInner.propTypes = {inner: PropTypes.number.isRequired}; + const Fn = React.memo(FnInner); + expect(() => { + ReactNoop.render(); + }).toWarnDev( + 'Invalid prop `inner` of type `string` supplied to `FnInner`, expected `number`.\n' + + ' in FnInner (at **)', + ); + }); + + it('validates propTypes declared on the outer component', () => { + function FnInner(props) { + return props.inner + props.outer; + } + const Fn = React.memo(FnInner); + Fn.propTypes = {outer: PropTypes.number.isRequired}; + expect(() => { + ReactNoop.render(); + }).toWarnDev( + 'Invalid prop `outer` of type `string` supplied to `FnInner`, expected `number`.\n' + + ' in FnInner (at **)', + ); + }); + + it('warns about propTypes declared on both the outer and the inner component', () => { + function FnInner(props) { + return props.inner + props.outer; + } + FnInner.propTypes = {inner: PropTypes.number.isRequired}; + const Fn = React.memo(FnInner); + Fn.propTypes = {outer: PropTypes.number.isRequired}; + expect(() => { + ReactNoop.render(); + }).toWarnDev( + [ + 'React.memo(FnInner): `propTypes` are defined both on the `React.memo()` result ' + + 'and on the inner `FnInner` component. Remove either one of these `propTypes` definitions.', + 'Invalid prop `outer` of type `string` supplied to `FnInner`, expected `number`.\n' + + ' in FnInner (at **)', + // Outer propTypes shadow the inner ones and aren't validated in this case. + ], + {withoutStack: 1}, + ); + + // Deduplication + ReactNoop.render(); + }); }); } }); diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js index ae095e4da3..4105fb0976 100644 --- a/packages/react/src/ReactElementValidator.js +++ b/packages/react/src/ReactElementValidator.js @@ -22,6 +22,7 @@ import { REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE, REACT_LAZY_TYPE, + REACT_MEMO_TYPE, } from 'shared/ReactSymbols'; import checkPropTypes from 'prop-types/checkPropTypes'; import warning from 'shared/warning'; @@ -34,9 +35,11 @@ import ReactDebugCurrentFrame, { } from './ReactDebugCurrentFrame'; let propTypesMisspellWarningShown; +let didWarnAboutDuplicatePropTypes; if (__DEV__) { propTypesMisspellWarningShown = false; + didWarnAboutDuplicatePropTypes = {}; } function getDeclarationErrorAddendum() { @@ -188,6 +191,23 @@ function getPropTypes(type) { switch (type.$$typeof) { case REACT_FORWARD_REF_TYPE: return type.propTypes; + case REACT_MEMO_TYPE: + const outerPropTypes = type.propTypes; + const innerPropTypes = type.type.propTypes; + if (outerPropTypes && innerPropTypes) { + const componentName = getComponentName(type); + if (!didWarnAboutDuplicatePropTypes[componentName]) { + didWarnAboutDuplicatePropTypes[componentName] = true; + warning( + false, + 'React.memo(%s): `propTypes` are defined both on the `React.memo()` result and on the ' + + 'inner `%s` component. Remove either one of these `propTypes` definitions.', + componentName, + componentName, + ); + } + } + return outerPropTypes || innerPropTypes; case REACT_LAZY_TYPE: const resolvedType = refineResolvedLazyComponent(type); if (resolvedType !== null) {