diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js
index 4bb6a4fedd..aab39a4265 100644
--- a/src/core/ReactCompositeComponent.js
+++ b/src/core/ReactCompositeComponent.js
@@ -29,6 +29,7 @@ var invariant = require('invariant');
var keyMirror = require('keyMirror');
var merge = require('merge');
var mixInto = require('mixInto');
+var objMap = require('objMap');
/**
* Policies that describe methods in `ReactCompositeComponentInterface`.
@@ -46,7 +47,13 @@ var SpecPolicy = keyMirror({
/**
* These methods are overriding the base ReactCompositeComponent class.
*/
- OVERRIDE_BASE: null
+ OVERRIDE_BASE: null,
+ /**
+ * These methods are similar to DEFINE_MANY, except we assume they return
+ * objects. We try to merge the keys of the return values of all the mixed in
+ * functions. If there is a key conflict we throw.
+ */
+ DEFINE_MANY_MERGED: null
});
/**
@@ -119,7 +126,7 @@ var ReactCompositeComponentInterface = {
* @return {object}
* @optional
*/
- getInitialState: SpecPolicy.DEFINE_ONCE,
+ getInitialState: SpecPolicy.DEFINE_MANY_MERGED,
/**
* Uses props from `this.props` and state from `this.state` to render the
@@ -301,7 +308,8 @@ function validateMethodOverride(proto, name) {
// Disallow defining methods more than once unless explicitly allowed.
if (proto.hasOwnProperty(name)) {
invariant(
- specPolicy === SpecPolicy.DEFINE_MANY,
+ specPolicy === SpecPolicy.DEFINE_MANY ||
+ specPolicy === SpecPolicy.DEFINE_MANY_MERGED,
'ReactCompositeComponentInterface: You are attempting to define ' +
'`%s` on your component more than once. This conflict may be due ' +
'to a mixin.',
@@ -366,7 +374,12 @@ function mixSpecIntoComponent(Constructor, spec) {
if (isInherited) {
// For methods which are defined more than once, call the existing
// methods before calling the new property.
- proto[name] = createChainedFunction(proto[name], property);
+ if (ReactCompositeComponentInterface[name] ===
+ SpecPolicy.DEFINE_MANY_MERGED) {
+ proto[name] = createMergedResultFunction(proto[name], property);
+ } else {
+ proto[name] = createChainedFunction(proto[name], property);
+ }
} else {
proto[name] = property;
}
@@ -375,6 +388,48 @@ function mixSpecIntoComponent(Constructor, spec) {
}
}
+/**
+ * Merge two objects, but throw if both contain the same key.
+ *
+ * @param {object} one The first object, which is mutated.
+ * @param {object} two The second object
+ * @return {object} one after it has been mutated to contain everything in two.
+ */
+function mergeObjectsWithNoDuplicateKeys(one, two) {
+ invariant(
+ one && two && typeof one === 'object' && typeof two === 'object',
+ 'mergeObjectsWithNoDuplicateKeys(): Cannot merge non-objects'
+ );
+
+ objMap(two, function(value, key) {
+ invariant(
+ one[key] === undefined,
+ 'mergeObjectsWithNoDuplicateKeys(): ' +
+ 'Tried to merge two objects with the same key: %s',
+ key
+ );
+ one[key] = value;
+ });
+ return one;
+}
+
+/**
+ * Creates a function that invokes two functions and merges their return values.
+ *
+ * @param {function} one Function to invoke first.
+ * @param {function} two Function to invoke second.
+ * @return {function} Function that invokes the two argument functions.
+ * @private
+ */
+function createMergedResultFunction(one, two) {
+ return function mergedResult() {
+ return mergeObjectsWithNoDuplicateKeys(
+ one.apply(this, arguments),
+ two.apply(this, arguments)
+ );
+ };
+}
+
/**
* Creates a function that invokes two functions and ignores their return vales.
*
diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js
index 0ee5268bb5..752ab19fb4 100644
--- a/src/core/__tests__/ReactCompositeComponent-test.js
+++ b/src/core/__tests__/ReactCompositeComponent-test.js
@@ -312,4 +312,73 @@ describe('ReactCompositeComponent', function() {
expect(ReactCurrentOwner.current).toBe(null);
});
+ it('should support mixins with getInitialState()', function() {
+ var Mixin = {
+ getInitialState: function() {
+ return {mixin: true};
+ }
+ };
+ var Component = React.createClass({
+ mixins: [Mixin],
+ getInitialState: function() {
+ return {component: true};
+ },
+ render: function() {
+ return ;
+ }
+ });
+ var instance = ;
+ ReactTestUtils.renderIntoDocument(instance);
+ expect(instance.state.component).toBe(true);
+ expect(instance.state.mixin).toBe(true);
+ });
+
+ it('should throw with conflicting getInitialState() methods', function() {
+ var Mixin = {
+ getInitialState: function() {
+ return {x: true};
+ }
+ };
+ var Component = React.createClass({
+ mixins: [Mixin],
+ getInitialState: function() {
+ return {x: true};
+ },
+ render: function() {
+ return ;
+ }
+ });
+ var instance = ;
+ expect(function() {
+ ReactTestUtils.renderIntoDocument(instance);
+ }).toThrow(
+ 'Invariant Violation: mergeObjectsWithNoDuplicateKeys(): ' +
+ 'Tried to merge two objects with the same key: x'
+ );
+ });
+
+ it('should throw with bad getInitialState() return values', function() {
+ var Mixin = {
+ getInitialState: function() {
+ return null;
+ }
+ };
+ var Component = React.createClass({
+ mixins: [Mixin],
+ getInitialState: function() {
+ return {x: true};
+ },
+ render: function() {
+ return ;
+ }
+ });
+ var instance = ;
+ expect(function() {
+ ReactTestUtils.renderIntoDocument(instance);
+ }).toThrow(
+ 'Invariant Violation: mergeObjectsWithNoDuplicateKeys(): ' +
+ 'Cannot merge non-objects'
+ );
+ });
+
});