/** * Copyright 2014-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactElementValidator */ /** * ReactElementValidator provides a wrapper around a element factory * which validates the props passed to the element. This is intended to be * used only in DEV and could be replaced by a static type checker for languages * that support it. */ 'use strict'; var ReactElement = require("./ReactElement"); var ReactFragment = require("./ReactFragment"); var ReactPropTypeLocations = require("./ReactPropTypeLocations"); var ReactPropTypeLocationNames = require("./ReactPropTypeLocationNames"); var ReactCurrentOwner = require("./ReactCurrentOwner"); var ReactNativeComponent = require("./ReactNativeComponent"); var getIteratorFn = require("./getIteratorFn"); var invariant = require("./invariant"); var warning = require("./warning"); function getDeclarationErrorAddendum() { if (ReactCurrentOwner.current) { var name = ReactCurrentOwner.current.getName(); if (name) { return ' Check the render method of `' + name + '`.'; } } return ''; } /** * Warn if there's no key explicitly set on dynamic arrays of children or * object keys are not valid. This allows us to keep track of children between * updates. */ var ownerHasKeyUseWarning = {}; var loggedTypeFailures = {}; var NUMERIC_PROPERTY_REGEX = /^\d+$/; /** * Gets the instance's name for use in warnings. * * @internal * @return {?string} Display name or undefined */ function getName(instance) { var publicInstance = instance && instance.getPublicInstance(); if (!publicInstance) { return undefined; } var constructor = publicInstance.constructor; if (!constructor) { return undefined; } return constructor.displayName || constructor.name || undefined; } /** * Gets the current owner's displayName for use in warnings. * * @internal * @return {?string} Display name or undefined */ function getCurrentOwnerDisplayName() { var current = ReactCurrentOwner.current; return ( current && getName(current) || undefined ); } /** * Warn if the element doesn't have an explicit key assigned to it. * This element is in an array. The array could grow and shrink or be * reordered. All children that haven't already been validated are required to * have a "key" property assigned to it. * * @internal * @param {ReactElement} element Element that requires a key. * @param {*} parentType element's parent's type. */ function validateExplicitKey(element, parentType) { if (element._store.validated || element.key != null) { return; } element._store.validated = true; warnAndMonitorForKeyUse( 'Each child in an array or iterator should have a unique "key" prop.', element, parentType ); } /** * Warn if the key is being defined as an object property but has an incorrect * value. * * @internal * @param {string} name Property name of the key. * @param {ReactElement} element Component that requires a key. * @param {*} parentType element's parent's type. */ function validatePropertyKey(name, element, parentType) { if (!NUMERIC_PROPERTY_REGEX.test(name)) { return; } warnAndMonitorForKeyUse( 'Child objects should have non-numeric keys so ordering is preserved.', element, parentType ); } /** * Shared warning and monitoring code for the key warnings. * * @internal * @param {string} message The base warning that gets output. * @param {ReactElement} element Component that requires a key. * @param {*} parentType element's parent's type. */ function warnAndMonitorForKeyUse(message, element, parentType) { var ownerName = getCurrentOwnerDisplayName(); var parentName = typeof parentType === 'string' ? parentType : parentType.displayName || parentType.name; var useName = ownerName || parentName; var memoizer = ownerHasKeyUseWarning[message] || ( (ownerHasKeyUseWarning[message] = {}) ); if (memoizer.hasOwnProperty(useName)) { return; } memoizer[useName] = true; var parentOrOwnerAddendum = ownerName ? (" Check the render method of " + ownerName + ".") : parentName ? (" Check the React.render call using <" + parentName + ">.") : ''; // Usually the current owner is the offender, but if it accepts children as a // property, it may be the creator of the child that's responsible for // assigning it a key. var childOwnerAddendum = ''; if (element && element._owner && element._owner !== ReactCurrentOwner.current) { // Name of the component that originally created this child. var childOwnerName = getName(element._owner); childOwnerAddendum = (" It was passed a child from " + childOwnerName + "."); } ("production" !== process.env.NODE_ENV ? warning( false, message + '%s%s See http://fb.me/react-warning-keys for more information.', parentOrOwnerAddendum, childOwnerAddendum ) : null); } /** * Ensure that every element either is passed in a static location, in an * array with an explicit keys property defined, or in an object literal * with valid key property. * * @internal * @param {ReactNode} node Statically passed child of any type. * @param {*} parentType node's parent's type. */ function validateChildKeys(node, parentType) { if (Array.isArray(node)) { for (var i = 0; i < node.length; i++) { var child = node[i]; if (ReactElement.isValidElement(child)) { validateExplicitKey(child, parentType); } } } else if (ReactElement.isValidElement(node)) { // This element was passed in a valid location. node._store.validated = true; } else if (node) { var iteratorFn = getIteratorFn(node); // Entry iterators provide implicit keys. if (iteratorFn) { if (iteratorFn !== node.entries) { var iterator = iteratorFn.call(node); var step; while (!(step = iterator.next()).done) { if (ReactElement.isValidElement(step.value)) { validateExplicitKey(step.value, parentType); } } } } else if (typeof node === 'object') { var fragment = ReactFragment.extractIfFragment(node); for (var key in fragment) { if (fragment.hasOwnProperty(key)) { validatePropertyKey(key, fragment[key], parentType); } } } } } /** * Assert that the props are valid * * @param {string} componentName Name of the component for error messages. * @param {object} propTypes Map of prop name to a ReactPropType * @param {object} props * @param {string} location e.g. "prop", "context", "child context" * @private */ function checkPropTypes(componentName, propTypes, props, location) { for (var propName in propTypes) { if (propTypes.hasOwnProperty(propName)) { var error; // Prop type validation may throw. In case they do, we don't want to // fail the render phase where it didn't fail before. So we log it. // After these have been cleaned up, we'll let them throw. try { // This is intentionally an invariant that gets caught. It's the same // behavior as without this statement except with a better message. ("production" !== process.env.NODE_ENV ? invariant( typeof propTypes[propName] === 'function', '%s: %s type `%s` is invalid; it must be a function, usually from ' + 'React.PropTypes.', componentName || 'React class', ReactPropTypeLocationNames[location], propName ) : invariant(typeof propTypes[propName] === 'function')); error = propTypes[propName](props, propName, componentName, location); } catch (ex) { error = ex; } if (error instanceof Error && !(error.message in loggedTypeFailures)) { // Only monitor this failure once because there tends to be a lot of the // same error. loggedTypeFailures[error.message] = true; var addendum = getDeclarationErrorAddendum(this); ("production" !== process.env.NODE_ENV ? warning(false, 'Failed propType: %s%s', error.message, addendum) : null); } } } } var warnedPropsMutations = {}; /** * Warn about mutating props when setting `propName` on `element`. * * @param {string} propName The string key within props that was set * @param {ReactElement} element */ function warnForPropsMutation(propName, element) { var type = element.type; var elementName = typeof type === 'string' ? type : type.displayName; var ownerName = element._owner ? element._owner.getPublicInstance().constructor.displayName : null; var warningKey = propName + '|' + elementName + '|' + ownerName; if (warnedPropsMutations.hasOwnProperty(warningKey)) { return; } warnedPropsMutations[warningKey] = true; var elementInfo = ''; if (elementName) { elementInfo = ' <' + elementName + ' />'; } var ownerInfo = ''; if (ownerName) { ownerInfo = ' The element was created by ' + ownerName + '.'; } ("production" !== process.env.NODE_ENV ? warning( false, 'Don\'t set .props.%s of the React component%s. Instead, specify the ' + 'correct value when initially creating the element or use ' + 'React.cloneElement to make a new element with updated props.%s', propName, elementInfo, ownerInfo ) : null); } // Inline Object.is polyfill function is(a, b) { if (a !== a) { // NaN return b !== b; } if (a === 0 && b === 0) { // +-0 return 1 / a === 1 / b; } return a === b; } /** * Given an element, check if its props have been mutated since element * creation (or the last call to this function). In particular, check if any * new props have been added, which we can't directly catch by defining warning * properties on the props object. * * @param {ReactElement} element */ function checkAndWarnForMutatedProps(element) { if (!element._store) { // Element was created using `new ReactElement` directly or with // `ReactElement.createElement`; skip mutation checking return; } var originalProps = element._store.originalProps; var props = element.props; for (var propName in props) { if (props.hasOwnProperty(propName)) { if (!originalProps.hasOwnProperty(propName) || !is(originalProps[propName], props[propName])) { warnForPropsMutation(propName, element); // Copy over the new value so that the two props objects match again originalProps[propName] = props[propName]; } } } } /** * Given an element, validate that its props follow the propTypes definition, * provided by the type. * * @param {ReactElement} element */ function validatePropTypes(element) { if (element.type == null) { // This has already warned. Don't throw. return; } // Extract the component class from the element. Converts string types // to a composite class which may have propTypes. // TODO: Validating a string's propTypes is not decoupled from the // rendering target which is problematic. var componentClass = ReactNativeComponent.getComponentClassForElement( element ); var name = componentClass.displayName || componentClass.name; if (componentClass.propTypes) { checkPropTypes( name, componentClass.propTypes, element.props, ReactPropTypeLocations.prop ); } if (typeof componentClass.getDefaultProps === 'function') { ("production" !== process.env.NODE_ENV ? warning( componentClass.getDefaultProps.isReactClassApproved, 'getDefaultProps is only used on classic React.createClass ' + 'definitions. Use a static property named `defaultProps` instead.' ) : null); } } var ReactElementValidator = { checkAndWarnForMutatedProps: checkAndWarnForMutatedProps, createElement: function(type, props, children) { // We warn in this case but don't throw. We expect the element creation to // succeed and there will likely be errors in render. ("production" !== process.env.NODE_ENV ? warning( type != null, 'React.createElement: type should not be null or undefined. It should ' + 'be a string (for DOM elements) or a ReactClass (for composite ' + 'components).' ) : null); var element = ReactElement.createElement.apply(this, arguments); // The result can be nullish if a mock or a custom function is used. // TODO: Drop this when these are no longer allowed as the type argument. if (element == null) { return element; } for (var i = 2; i < arguments.length; i++) { validateChildKeys(arguments[i], type); } validatePropTypes(element); return element; }, createFactory: function(type) { var validatedFactory = ReactElementValidator.createElement.bind( null, type ); // Legacy hook TODO: Warn if this is accessed validatedFactory.type = type; if ("production" !== process.env.NODE_ENV) { try { Object.defineProperty( validatedFactory, 'type', { enumerable: false, get: function() { ("production" !== process.env.NODE_ENV ? warning( false, 'Factory.type is deprecated. Access the class directly ' + 'before passing it to createFactory.' ) : null); Object.defineProperty(this, 'type', { value: type }); return type; } } ); } catch (x) { // IE will fail on defineProperty (es5-shim/sham too) } } return validatedFactory; }, cloneElement: function(element, props, children) { var newElement = ReactElement.cloneElement.apply(this, arguments); for (var i = 2; i < arguments.length; i++) { validateChildKeys(arguments[i], newElement.type); } validatePropTypes(newElement); return newElement; } }; module.exports = ReactElementValidator;