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