diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index 562dadf675..aabbfb4e86 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -91,6 +91,19 @@ var ReactCompositeComponentInterface = { // ==== Definition methods ==== + /** + * Invoked when the component is mounted and whenever new props are received. + * Values in the returned mapping will be set on `this.props` if that prop is + * not specified (i.e. using an `in` check). + * + * This method is invoked before `getInitialState` and therefore cannot rely + * on `this.state` or use `this.setState`. + * + * @return {object} + * @optional + */ + getDefaultProps: SpecPolicy.DEFINE_ONCE, + /** * Invoked once before the component is mounted. The return value will be used * as the initial value of `this.state`. @@ -419,9 +432,7 @@ var ReactCompositeComponentMixin = { this._lifeCycleState = ReactComponent.LifeCycle.UNMOUNTED; this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING; - if (this.constructor.propDeclarations) { - this._assertValidProps(this.props); - } + this._processProps(this.props); if (this.__reactAutoBindMap) { this._bindAutoBindMethods(); @@ -489,9 +500,7 @@ var ReactCompositeComponentMixin = { * @internal */ receiveProps: function(nextProps, transaction) { - if (this.constructor.propDeclarations) { - this._assertValidProps(nextProps); - } + this._processProps(nextProps); ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction); this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS; @@ -573,6 +582,35 @@ var ReactCompositeComponentMixin = { } }, + /** + * Processes props by setting default values for unspecified props and + * asserting that the props are valid. + * + * @param {object} props + * @private + */ + _processProps: function(props) { + var propName; + if (this.getDefaultProps) { + var defaultProps = this.getDefaultProps(); + for (propName in defaultProps) { + if (!(propName in props)) { + props[propName] = defaultProps[propName]; + } + } + } + var propDeclarations = this.constructor.propDeclarations; + if (propDeclarations) { + var componentName = this.constructor.displayName; + for (propName in propDeclarations) { + var checkProp = propDeclarations[propName]; + if (checkProp) { + checkProp(props, propName, componentName); + } + } + } + }, + /** * Receives next props and next state, and negotiates whether or not the * component should update as a result. @@ -696,21 +734,6 @@ var ReactCompositeComponentMixin = { return renderedComponent; }, - /** - * @param {object} props - * @private - */ - _assertValidProps: function(props) { - var propDeclarations = this.constructor.propDeclarations; - var componentName = this.constructor.displayName; - for (var propName in propDeclarations) { - var checkProp = propDeclarations[propName]; - if (checkProp) { - checkProp(props, propName, componentName); - } - } - }, - /** * @private */ diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js index 463bfd6144..51575277b3 100644 --- a/src/core/__tests__/ReactCompositeComponent-test.js +++ b/src/core/__tests__/ReactCompositeComponent-test.js @@ -194,6 +194,53 @@ describe('ReactCompositeComponent', function() { expect(retValAfterMountWithCrazyScope).toBe(RETURN_VALUE_AFTER_MOUNT); }); + it('should normalize props with default values', function() { + var Component = React.createClass({ + props: {key: ReactProps.string.isRequired}, + getDefaultProps: function() { + return {key: 'testKey'}; + }, + getInitialState: function() { + return {key: this.props.key + 'State'}; + }, + render: function() { + return {this.props.key}; + } + }); + + var instance = ; + ReactTestUtils.renderIntoDocument(instance); + reactComponentExpect(instance).scalarPropsEqual({key: 'testKey'}); + reactComponentExpect(instance).scalarStateEqual({key: 'testKeyState'}); + + expect(function() { + ReactTestUtils.renderIntoDocument(); + }).toThrow( + 'Invariant Violation: Required prop `key` was not specified in ' + + '`Component`.' + ); + }); + + it('should check default prop values', function() { + var Component = React.createClass({ + props: {key: ReactProps.string.isRequired}, + getDefaultProps: function() { + return {key: null}; + }, + render: function() { + return {this.props.key}; + } + }); + + var instance = ; + expect(function() { + ReactTestUtils.renderIntoDocument(instance); + }).toThrow( + 'Invariant Violation: Required prop `key` was not specified in ' + + '`Component`.' + ); + }); + it('should check declared prop types', function() { var Component = React.createClass({ props: {