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) {