Allow getInitialState() for mixins
Today mixins can't easily be stateful because they can't provide getInitialState(). This allows multiple getInitialState() methods as long as they don't return objects that have conflicting keys. In that case, we throw.
Pete Hunt committed
Aug 27, 2013 at 14:16 UTC
adb666e67fa2a4a525a641f6fa7a239c3dd1cd1c
2 files changed
+128
-4
src/core/ReactCompositeComponent.js
+59
-4
@@ -29,6 +29,7 @@ var invariant = require('invariant');
29
var keyMirror = require('keyMirror');
30
var merge = require('merge');
31
var mixInto = require('mixInto');
32
+var objMap = require('objMap');
33
34
/**
35
* Policies that describe methods in `ReactCompositeComponentInterface`.
@@ -46,7 +47,13 @@ var SpecPolicy = keyMirror({
47
/**
48
* These methods are overriding the base ReactCompositeComponent class.
49
*/
49
- OVERRIDE_BASE: null
50
+ OVERRIDE_BASE: null,
51
+ /**
52
+ * These methods are similar to DEFINE_MANY, except we assume they return
53
+ * objects. We try to merge the keys of the return values of all the mixed in
54
+ * functions. If there is a key conflict we throw.
55
+ */
56
+ DEFINE_MANY_MERGED: null
57
});
58
59
/**
@@ -119,7 +126,7 @@ var ReactCompositeComponentInterface = {
126
* @return {object}
127
* @optional
128
*/
122
- getInitialState: SpecPolicy.DEFINE_ONCE,
129
+ getInitialState: SpecPolicy.DEFINE_MANY_MERGED,
130
131
/**
132
* Uses props from `this.props` and state from `this.state` to render the
@@ -301,7 +308,8 @@ function validateMethodOverride(proto, name) {
308
// Disallow defining methods more than once unless explicitly allowed.
309
if (proto.hasOwnProperty(name)) {
310
invariant(
304
- specPolicy === SpecPolicy.DEFINE_MANY,
311
+ specPolicy === SpecPolicy.DEFINE_MANY ||
312
+ specPolicy === SpecPolicy.DEFINE_MANY_MERGED,
313
'ReactCompositeComponentInterface: You are attempting to define ' +
314
'`%s` on your component more than once. This conflict may be due ' +
315
'to a mixin.',
@@ -366,7 +374,12 @@ function mixSpecIntoComponent(Constructor, spec) {
374
if (isInherited) {
375
// For methods which are defined more than once, call the existing
376
// methods before calling the new property.
369
- proto[name] = createChainedFunction(proto[name], property);
377
+ if (ReactCompositeComponentInterface[name] ===
378
+ SpecPolicy.DEFINE_MANY_MERGED) {
379
+ proto[name] = createMergedResultFunction(proto[name], property);
380
+ } else {
381
+ proto[name] = createChainedFunction(proto[name], property);
382
+ }
383
} else {
384
proto[name] = property;
385
}
@@ -375,6 +388,48 @@ function mixSpecIntoComponent(Constructor, spec) {
388
}
389
}
390
391
+/**
392
+ * Merge two objects, but throw if both contain the same key.
393
+ *
394
+ * @param {object} one The first object, which is mutated.
395
+ * @param {object} two The second object
396
+ * @return {object} one after it has been mutated to contain everything in two.
397
+ */
398
+function mergeObjectsWithNoDuplicateKeys(one, two) {
399
+ invariant(
400
+ one && two && typeof one === 'object' && typeof two === 'object',
401
+ 'mergeObjectsWithNoDuplicateKeys(): Cannot merge non-objects'
402
+ );
403
+
404
+ objMap(two, function(value, key) {
405
+ invariant(
406
+ one[key] === undefined,
407
+ 'mergeObjectsWithNoDuplicateKeys(): ' +
408
+ 'Tried to merge two objects with the same key: %s',
409
+ key
410
+ );
411
+ one[key] = value;
412
+ });
413
+ return one;
414
+}
415
+
416
+/**
417
+ * Creates a function that invokes two functions and merges their return values.
418
+ *
419
+ * @param {function} one Function to invoke first.
420
+ * @param {function} two Function to invoke second.
421
+ * @return {function} Function that invokes the two argument functions.
422
+ * @private
423
+ */
424
+function createMergedResultFunction(one, two) {
425
+ return function mergedResult() {
426
+ return mergeObjectsWithNoDuplicateKeys(
427
+ one.apply(this, arguments),
428
+ two.apply(this, arguments)
429
+ );
430
+ };
431
+}
432
+
433
/**
434
* Creates a function that invokes two functions and ignores their return vales.
435
*
src/core/__tests__/ReactCompositeComponent-test.js
+69
@@ -312,4 +312,73 @@ describe('ReactCompositeComponent', function() {
312
expect(ReactCurrentOwner.current).toBe(null);
313
});
314
315
+ it('should support mixins with getInitialState()', function() {
316
+ var Mixin = {
317
+ getInitialState: function() {
318
+ return {mixin: true};
319
+ }
320
+ };
321
+ var Component = React.createClass({
322
+ mixins: [Mixin],
323
+ getInitialState: function() {
324
+ return {component: true};
325
+ },
326
+ render: function() {
327
+ return <span />;
328
+ }
329
+ });
330
+ var instance = <Component />;
331
+ ReactTestUtils.renderIntoDocument(instance);
332
+ expect(instance.state.component).toBe(true);
333
+ expect(instance.state.mixin).toBe(true);
334
+ });
335
+
336
+ it('should throw with conflicting getInitialState() methods', function() {
337
+ var Mixin = {
338
+ getInitialState: function() {
339
+ return {x: true};
340
+ }
341
+ };
342
+ var Component = React.createClass({
343
+ mixins: [Mixin],
344
+ getInitialState: function() {
345
+ return {x: true};
346
+ },
347
+ render: function() {
348
+ return <span />;
349
+ }
350
+ });
351
+ var instance = <Component />;
352
+ expect(function() {
353
+ ReactTestUtils.renderIntoDocument(instance);
354
+ }).toThrow(
355
+ 'Invariant Violation: mergeObjectsWithNoDuplicateKeys(): ' +
356
+ 'Tried to merge two objects with the same key: x'
357
+ );
358
+ });
359
+
360
+ it('should throw with bad getInitialState() return values', function() {
361
+ var Mixin = {
362
+ getInitialState: function() {
363
+ return null;
364
+ }
365
+ };
366
+ var Component = React.createClass({
367
+ mixins: [Mixin],
368
+ getInitialState: function() {
369
+ return {x: true};
370
+ },
371
+ render: function() {
372
+ return <span />;
373
+ }
374
+ });
375
+ var instance = <Component />;
376
+ expect(function() {
377
+ ReactTestUtils.renderIntoDocument(instance);
378
+ }).toThrow(
379
+ 'Invariant Violation: mergeObjectsWithNoDuplicateKeys(): ' +
380
+ 'Cannot merge non-objects'
381
+ );
382
+ });
383
+
384
});