diff --git a/README.md b/README.md index 0b832e9..1a23c9d 100644 --- a/README.md +++ b/README.md @@ -78,9 +78,12 @@ MyComponent.propTypes = { // (or fragment) containing these types. optionalNode: PropTypes.node, - // A React element. + // A React element (ie. ). optionalElement: PropTypes.element, + // A React element type (ie. MyComponent). + optionalElementType: PropTypes.elementType, + // You can also declare that a prop is an instance of a class. This uses // JS's instanceof operator. optionalMessage: PropTypes.instanceOf(Message), diff --git a/__tests__/PropTypesDevelopmentStandalone-test.js b/__tests__/PropTypesDevelopmentStandalone-test.js index b2ffaa1..e65c1d3 100644 --- a/__tests__/PropTypesDevelopmentStandalone-test.js +++ b/__tests__/PropTypesDevelopmentStandalone-test.js @@ -18,6 +18,7 @@ function resetWarningCache() { React = require('react'); PropTypes = require('../index'); } +resetWarningCache(); function getPropTypeWarningMessage(propTypes, object, componentName) { if (!console.error.calls) { @@ -555,6 +556,77 @@ describe('PropTypesDevelopmentStandalone', () => { }); }); + describe('ElementType Types', () => { + it('should support native component', () => { + typeCheckPass(PropTypes.elementType, 'div'); + }); + + it('should support stateless component', () => { + var MyComponent = () =>
; + typeCheckPass(PropTypes.elementType, MyComponent); + }); + + it('should support stateful component', () => { + class MyComponent extends React.Component { + render() { + return
; + } + } + typeCheckPass(PropTypes.elementType, MyComponent); + }); + + (React.forwardRef ? it : it.skip)('should support forwardRef component', () => { + const MyForwardRef = React.forwardRef((props, ref) =>
); + typeCheckPass(PropTypes.elementType, MyForwardRef); + }); + + (React.createContext ? it : it.skip)('should support context provider component', () => { + const MyContext = React.createContext('test'); + typeCheckPass(PropTypes.elementType, MyContext.Provider); + }); + + (React.createContext ? it : it.skip)('should support context consumer component', () => { + const MyContext = React.createContext('test'); + typeCheckPass(PropTypes.elementType, MyContext.Consumer); + }); + + it('should warn for invalid types', () => { + typeCheckFail( + PropTypes.elementType, + true, + 'Invalid prop `testProp` of type `boolean` supplied to ' + + '`testComponent`, expected a single ReactElement type.', + ); + + typeCheckFail( + PropTypes.elementType, + {}, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected a single ReactElement type.', + ); + + typeCheckFail( + PropTypes.elementType, + [], + 'Invalid prop `testProp` of type `array` supplied to ' + + '`testComponent`, expected a single ReactElement type.', + ); + + it('should warn for missing required values', () => { + typeCheckFailRequiredValues(PropTypes.elementType.isRequired); + }); + }); + + it('should warn for React element', () => { + typeCheckFail( + PropTypes.elementType, +
, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected a single ReactElement type.', + ); + }); + }); + describe('Instance Types', () => { it('should warn for invalid instances', () => { function Person() {} diff --git a/__tests__/PropTypesProductionStandalone-test.js b/__tests__/PropTypesProductionStandalone-test.js index 3b2693c..1e2ca0b 100644 --- a/__tests__/PropTypesProductionStandalone-test.js +++ b/__tests__/PropTypesProductionStandalone-test.js @@ -162,6 +162,13 @@ describe('PropTypesProductionStandalone', function() { }); }); + describe('React ElementType Type', function() { + it('shoud be a no-op', function() { + expectThrowsInProduction(PropTypes.elementType.isRequired, false); + expectThrowsInProduction(PropTypes.elementType.isRequired, {}); + }); + }); + describe('ObjectOf Type', function() { it('should be a no-op', function() { expectThrowsInProduction(PropTypes.objectOf({foo: PropTypes.string}), { diff --git a/factoryWithThrowingShims.js b/factoryWithThrowingShims.js index e0f8062..e5b2f9c 100644 --- a/factoryWithThrowingShims.js +++ b/factoryWithThrowingShims.js @@ -45,6 +45,7 @@ module.exports = function() { any: shim, arrayOf: getShim, element: shim, + elementType: shim, instanceOf: getShim, node: shim, objectOf: getShim, diff --git a/factoryWithTypeCheckers.js b/factoryWithTypeCheckers.js index 1b73c3b..1a5f759 100644 --- a/factoryWithTypeCheckers.js +++ b/factoryWithTypeCheckers.js @@ -7,6 +7,7 @@ 'use strict'; +var ReactIs = require('react-is'); var assign = require('object-assign'); var ReactPropTypesSecret = require('./lib/ReactPropTypesSecret'); @@ -123,6 +124,7 @@ module.exports = function(isValidElement, throwOnDirectAccess) { any: createAnyTypeChecker(), arrayOf: createArrayOfTypeChecker, element: createElementTypeChecker(), + elementType: createElementTypeTypeChecker(), instanceOf: createInstanceTypeChecker, node: createNodeChecker(), objectOf: createObjectOfTypeChecker, @@ -276,6 +278,18 @@ module.exports = function(isValidElement, throwOnDirectAccess) { return createChainableTypeChecker(validate); } + function createElementTypeTypeChecker() { + function validate(props, propName, componentName, location, propFullName) { + var propValue = props[propName]; + if (!ReactIs.isValidElementType(propValue)) { + var propType = getPropType(propValue); + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected a single ReactElement type.')); + } + return null; + } + return createChainableTypeChecker(validate); + } + function createInstanceTypeChecker(expectedClass) { function validate(props, propName, componentName, location, propFullName) { if (!(props[propName] instanceof expectedClass)) { diff --git a/index.js b/index.js index 11bfcea..e9ef51d 100644 --- a/index.js +++ b/index.js @@ -6,21 +6,12 @@ */ if (process.env.NODE_ENV !== 'production') { - var REACT_ELEMENT_TYPE = (typeof Symbol === 'function' && - Symbol.for && - Symbol.for('react.element')) || - 0xeac7; - - var isValidElement = function(object) { - return typeof object === 'object' && - object !== null && - object.$$typeof === REACT_ELEMENT_TYPE; - }; + var ReactIs = require('react-is'); // By explicitly using `prop-types` you are opting into new development behavior. // http://fb.me/prop-types-in-prod var throwOnDirectAccess = true; - module.exports = require('./factoryWithTypeCheckers')(isValidElement, throwOnDirectAccess); + module.exports = require('./factoryWithTypeCheckers')(ReactIs.isElement, throwOnDirectAccess); } else { // By explicitly using `prop-types` you are opting into new production behavior. // http://fb.me/prop-types-in-prod diff --git a/package.json b/package.json index 0f47058..98e15ab 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ }, "homepage": "https://facebook.github.io/react/", "dependencies": { - "object-assign": "^4.1.1" + "object-assign": "^4.1.1", + "react-is": "^16.8.1" }, "scripts": { "test": "jest", diff --git a/yarn.lock b/yarn.lock index 67d038a..b32117b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2356,6 +2356,11 @@ randombytes@^2.0.0, randombytes@^2.0.1: dependencies: safe-buffer "^5.1.0" +react-is@^16.8.1: + version "16.8.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.1.tgz#a80141e246eb894824fb4f2901c0c50ef31d4cdb" + integrity sha512-ioMCzVDWvCvKD8eeT+iukyWrBGrA3DiFYkXfBsVYIRdaREZuBjENG+KjrikavCLasozqRWTwFUagU/O4vPpRMA== + react@^15.5.1: version "15.6.2" resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"