diff --git a/packages/react/src/ReactElementValidator.js b/packages/react/src/ReactElementValidator.js
index 2142c4783e3b0..35dc391f435a2 100644
--- a/packages/react/src/ReactElementValidator.js
+++ b/packages/react/src/ReactElementValidator.js
@@ -16,7 +16,11 @@ import lowPriorityWarning from 'shared/lowPriorityWarning';
import describeComponentFrame from 'shared/describeComponentFrame';
import isValidElementType from 'shared/isValidElementType';
import getComponentName from 'shared/getComponentName';
-import {getIteratorFn, REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols';
+import {
+ getIteratorFn,
+ REACT_FORWARD_REF_TYPE,
+ REACT_FRAGMENT_TYPE,
+} from 'shared/ReactSymbols';
import checkPropTypes from 'prop-types/checkPropTypes';
import warning from 'fbjs/lib/warning';
@@ -42,10 +46,20 @@ if (__DEV__) {
return '#text';
} else if (typeof element.type === 'string') {
return element.type;
- } else if (element.type === REACT_FRAGMENT_TYPE) {
+ }
+
+ const type = element.type;
+ if (type === REACT_FRAGMENT_TYPE) {
return 'React.Fragment';
+ } else if (
+ typeof type === 'object' &&
+ type !== null &&
+ type.$$typeof === REACT_FORWARD_REF_TYPE
+ ) {
+ const functionName = type.render.displayName || type.render.name || '';
+ return functionName !== '' ? `ForwardRef(${functionName})` : 'ForwardRef';
} else {
- return element.type.displayName || element.type.name || 'Unknown';
+ return type.displayName || type.name || 'Unknown';
}
};
@@ -213,20 +227,29 @@ function validateChildKeys(node, parentType) {
* @param {ReactElement} element
*/
function validatePropTypes(element) {
- const componentClass = element.type;
- if (typeof componentClass !== 'function') {
+ const type = element.type;
+ let name, propTypes;
+ if (typeof type === 'function') {
+ // Class or functional component
+ name = type.displayName || type.name;
+ propTypes = type.propTypes;
+ } else if (
+ typeof type === 'object' &&
+ type !== null &&
+ type.$$typeof === REACT_FORWARD_REF_TYPE
+ ) {
+ // ForwardRef
+ const functionName = type.render.displayName || type.render.name || '';
+ name = functionName !== '' ? `ForwardRef(${functionName})` : 'ForwardRef';
+ propTypes = type.propTypes;
+ } else {
return;
}
- const name = componentClass.displayName || componentClass.name;
- const propTypes = componentClass.propTypes;
if (propTypes) {
currentlyValidatingElement = element;
checkPropTypes(propTypes, element.props, 'prop', name, getStackAddendum);
currentlyValidatingElement = null;
- } else if (
- componentClass.PropTypes !== undefined &&
- !propTypesMisspellWarningShown
- ) {
+ } else if (type.PropTypes !== undefined && !propTypesMisspellWarningShown) {
propTypesMisspellWarningShown = true;
warning(
false,
@@ -234,9 +257,9 @@ function validatePropTypes(element) {
name || 'Unknown',
);
}
- if (typeof componentClass.getDefaultProps === 'function') {
+ if (typeof type.getDefaultProps === 'function') {
warning(
- componentClass.getDefaultProps.isReactClassApproved,
+ type.getDefaultProps.isReactClassApproved,
'getDefaultProps is only used on classic React.createClass ' +
'definitions. Use a static property named `defaultProps` instead.',
);
diff --git a/packages/react/src/__tests__/forwardRef-test.internal.js b/packages/react/src/__tests__/forwardRef-test.internal.js
index a578ae73680fe..358ed49e04b36 100644
--- a/packages/react/src/__tests__/forwardRef-test.internal.js
+++ b/packages/react/src/__tests__/forwardRef-test.internal.js
@@ -94,31 +94,6 @@ describe('forwardRef', () => {
expect(ref.current instanceof Child).toBe(true);
});
- it('should update refs when switching between children', () => {
- function FunctionalComponent({forwardedRef, setRefOnDiv}) {
- return (
-
- );
- }
-
- const RefForwardingComponent = React.forwardRef((props, ref) => (
-
- ));
-
- const ref = React.createRef();
-
- ReactNoop.render();
- ReactNoop.flush();
- expect(ref.current.type).toBe('div');
-
- ReactNoop.render();
- ReactNoop.flush();
- expect(ref.current.type).toBe('span');
- });
-
it('should maintain child instance and ref through updates', () => {
class Child extends React.Component {
constructor(props) {
@@ -206,32 +181,6 @@ describe('forwardRef', () => {
expect(ref.current).toBe(null);
});
- it('should support rendering null', () => {
- const RefForwardingComponent = React.forwardRef((props, ref) => null);
-
- const ref = React.createRef();
-
- ReactNoop.render();
- ReactNoop.flush();
- expect(ref.current).toBe(null);
- });
-
- it('should support rendering null for multiple children', () => {
- const RefForwardingComponent = React.forwardRef((props, ref) => null);
-
- const ref = React.createRef();
-
- ReactNoop.render(
-
,
- );
- ReactNoop.flush();
- expect(ref.current).toBe(null);
- });
-
it('should not re-run the render callback on a deep setState', () => {
let inst;
@@ -264,43 +213,4 @@ describe('forwardRef', () => {
inst.setState({});
expect(ReactNoop.flush()).toEqual(['Inner']);
});
-
- it('should warn if not provided a callback during creation', () => {
- expect(() => React.forwardRef(undefined)).toWarnDev(
- 'forwardRef requires a render function but was given undefined.',
- );
- expect(() => React.forwardRef(null)).toWarnDev(
- 'forwardRef requires a render function but was given null.',
- );
- expect(() => React.forwardRef('foo')).toWarnDev(
- 'forwardRef requires a render function but was given string.',
- );
- });
-
- it('should warn if no render function is provided', () => {
- expect(React.forwardRef).toWarnDev(
- 'forwardRef requires a render function but was given undefined.',
- );
- });
-
- it('should warn if the render function provided has propTypes or defaultProps attributes', () => {
- function renderWithPropTypes() {
- return null;
- }
- renderWithPropTypes.propTypes = {};
-
- function renderWithDefaultProps() {
- return null;
- }
- renderWithDefaultProps.defaultProps = {};
-
- expect(() => React.forwardRef(renderWithPropTypes)).toWarnDev(
- 'forwardRef render functions do not support propTypes or defaultProps. ' +
- 'Did you accidentally pass a React component?',
- );
- expect(() => React.forwardRef(renderWithDefaultProps)).toWarnDev(
- 'forwardRef render functions do not support propTypes or defaultProps. ' +
- 'Did you accidentally pass a React component?',
- );
- });
});
diff --git a/packages/react/src/__tests__/forwardRef-test.js b/packages/react/src/__tests__/forwardRef-test.js
new file mode 100644
index 0000000000000..aa1cd160d5d14
--- /dev/null
+++ b/packages/react/src/__tests__/forwardRef-test.js
@@ -0,0 +1,158 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+describe('forwardRef', () => {
+ let PropTypes;
+ let React;
+ let ReactNoop;
+
+ beforeEach(() => {
+ jest.resetModules();
+ PropTypes = require('prop-types');
+ React = require('react');
+ ReactNoop = require('react-noop-renderer');
+ });
+
+ it('should update refs when switching between children', () => {
+ function FunctionalComponent({forwardedRef, setRefOnDiv}) {
+ return (
+
+ );
+ }
+
+ const RefForwardingComponent = React.forwardRef((props, ref) => (
+
+ ));
+
+ const ref = React.createRef();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ref.current.type).toBe('div');
+
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ref.current.type).toBe('span');
+ });
+
+ it('should support rendering null', () => {
+ const RefForwardingComponent = React.forwardRef((props, ref) => null);
+
+ const ref = React.createRef();
+
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ref.current).toBe(null);
+ });
+
+ it('should support rendering null for multiple children', () => {
+ const RefForwardingComponent = React.forwardRef((props, ref) => null);
+
+ const ref = React.createRef();
+
+ ReactNoop.render(
+ ,
+ );
+ ReactNoop.flush();
+ expect(ref.current).toBe(null);
+ });
+
+ it('should support propTypes and defaultProps', () => {
+ function FunctionalComponent({forwardedRef, optional, required}) {
+ return (
+
+ {optional}
+ {required}
+
+ );
+ }
+
+ const RefForwardingComponent = React.forwardRef(function NamedFunction(
+ props,
+ ref,
+ ) {
+ return ;
+ });
+ RefForwardingComponent.propTypes = {
+ optional: PropTypes.string,
+ required: PropTypes.string.isRequired,
+ };
+ RefForwardingComponent.defaultProps = {
+ optional: 'default',
+ };
+
+ const ref = React.createRef();
+
+ ReactNoop.render(
+ ,
+ );
+ ReactNoop.flush();
+ expect(ref.current.children).toEqual([{text: 'foo'}, {text: 'bar'}]);
+
+ ReactNoop.render();
+ ReactNoop.flush();
+ expect(ref.current.children).toEqual([{text: 'default'}, {text: 'foo'}]);
+
+ expect(() =>
+ ReactNoop.render(),
+ ).toWarnDev(
+ 'Warning: Failed prop type: The prop `required` is marked as required in ' +
+ '`ForwardRef(NamedFunction)`, but its value is `undefined`.\n' +
+ ' in ForwardRef(NamedFunction) (at **)',
+ );
+ });
+
+ it('should warn if not provided a callback during creation', () => {
+ expect(() => React.forwardRef(undefined)).toWarnDev(
+ 'forwardRef requires a render function but was given undefined.',
+ );
+ expect(() => React.forwardRef(null)).toWarnDev(
+ 'forwardRef requires a render function but was given null.',
+ );
+ expect(() => React.forwardRef('foo')).toWarnDev(
+ 'forwardRef requires a render function but was given string.',
+ );
+ });
+
+ it('should warn if no render function is provided', () => {
+ expect(React.forwardRef).toWarnDev(
+ 'forwardRef requires a render function but was given undefined.',
+ );
+ });
+
+ it('should warn if the render function provided has propTypes or defaultProps attributes', () => {
+ function renderWithPropTypes() {
+ return null;
+ }
+ renderWithPropTypes.propTypes = {};
+
+ function renderWithDefaultProps() {
+ return null;
+ }
+ renderWithDefaultProps.defaultProps = {};
+
+ expect(() => React.forwardRef(renderWithPropTypes)).toWarnDev(
+ 'forwardRef render functions do not support propTypes or defaultProps. ' +
+ 'Did you accidentally pass a React component?',
+ );
+ expect(() => React.forwardRef(renderWithDefaultProps)).toWarnDev(
+ 'forwardRef render functions do not support propTypes or defaultProps. ' +
+ 'Did you accidentally pass a React component?',
+ );
+ });
+});