Introduce 'statics' for class specification
This moves the static properties off of the root object and into a 'statics' property. Then they are made available on the constructor so they can be used directly and in mixins.
Marshall Roch committed
Dec 2, 2013 at 17:20 UTC
79b9d5af627a6f0e5c6c3496d7e3161df871f899
2 files changed
+309
-94
src/core/ReactCompositeComponent.js
+130
-57
@@ -85,38 +85,39 @@ var SpecPolicy = keyMirror({
85
*/
86
var ReactCompositeComponentInterface = {
87
88
- /**
89
- * An array of Mixin objects to include when defining your component.
90
- *
91
- * @type {array}
92
- * @optional
93
- */
94
- mixins: SpecPolicy.DEFINE_MANY,
95
-
96
- /**
97
- * Definition of prop types for this component.
98
- *
99
- * @type {object}
100
- * @optional
101
- */
102
- propTypes: SpecPolicy.DEFINE_MANY_MERGED,
103
-
104
- /**
105
- * Definition of context types for this component.
106
- *
107
- * @type {object}
108
- * @optional
109
- */
110
- contextTypes: SpecPolicy.DEFINE_MANY_MERGED,
111
-
112
- /**
113
- * Definition of context types this component sets for its children.
114
- *
115
- * @type {object}
116
- * @optional
117
- */
118
- childContextTypes: SpecPolicy.DEFINE_MANY_MERGED,
119
-
88
+ statics: {
89
+ /**
90
+ * An array of Mixin objects to include when defining your component.
91
+ *
92
+ * @type {array}
93
+ * @optional
94
+ */
95
+ mixins: SpecPolicy.DEFINE_MANY,
96
+
97
+ /**
98
+ * Definition of prop types for this component.
99
+ *
100
+ * @type {object}
101
+ * @optional
102
+ */
103
+ propTypes: SpecPolicy.DEFINE_MANY_MERGED,
104
+
105
+ /**
106
+ * Definition of context types for this component.
107
+ *
108
+ * @type {object}
109
+ * @optional
110
+ */
111
+ contextTypes: SpecPolicy.DEFINE_MANY_MERGED,
112
+
113
+ /**
114
+ * Definition of context types this component sets for its children.
115
+ *
116
+ * @type {object}
117
+ * @optional
118
+ */
119
+ childContextTypes: SpecPolicy.DEFINE_MANY_MERGED
120
+ },
121
122
// ==== Definition methods ====
123
@@ -303,23 +304,30 @@ var ReactCompositeComponentInterface = {
304
};
305
306
/**
306
- * Mapping from class specification keys to special processing functions.
307
+ * Mapping from class specification static keys to special processing functions.
308
+ *
309
+ * These are all defined inside the "statics" key in the class spec:
310
+ *
311
+ * React.createClass({
312
+ * statics: {
313
+ * mixins: [FooMixin]
314
+ * },
315
+ * render: function() {...}
316
+ * })
317
*
318
* Although these are declared in the specification when defining classes
319
* using `React.createClass`, they will not be on the component's prototype.
320
*/
311
-var RESERVED_SPEC_KEYS = {
312
- displayName: function(Constructor, displayName) {
313
- Constructor.displayName = displayName;
314
- },
315
- mixins: function(Constructor, mixins) {
321
+var RESERVED_STATIC_SPEC_KEYS = {
322
+ mixins: function(ConvenienceConstructor, mixins) {
323
if (mixins) {
324
for (var i = 0; i < mixins.length; i++) {
318
- mixSpecIntoComponent(Constructor, mixins[i]);
325
+ mixSpecIntoComponent(ConvenienceConstructor, mixins[i]);
326
}
327
}
328
},
322
- childContextTypes: function(Constructor, childContextTypes) {
329
+ childContextTypes: function(ConvenienceConstructor, childContextTypes) {
330
+ var Constructor = ConvenienceConstructor.componentConstructor;
331
validateTypeDef(
332
Constructor,
333
childContextTypes,
@@ -327,7 +335,8 @@ var RESERVED_SPEC_KEYS = {
335
);
336
Constructor.childContextTypes = childContextTypes;
337
},
330
- contextTypes: function(Constructor, contextTypes) {
338
+ contextTypes: function(ConvenienceConstructor, contextTypes) {
339
+ var Constructor = ConvenienceConstructor.componentConstructor;
340
validateTypeDef(
341
Constructor,
342
contextTypes,
@@ -335,7 +344,8 @@ var RESERVED_SPEC_KEYS = {
344
);
345
Constructor.contextTypes = contextTypes;
346
},
338
- propTypes: function(Constructor, propTypes) {
347
+ propTypes: function(ConvenienceConstructor, propTypes) {
348
+ var Constructor = ConvenienceConstructor.componentConstructor;
349
validateTypeDef(
350
Constructor,
351
propTypes,
@@ -345,6 +355,24 @@ var RESERVED_SPEC_KEYS = {
355
}
356
};
357
358
+/**
359
+ * Mapping from class specification keys to special processing functions.
360
+ *
361
+ * Although these are declared in the specification when defining classes
362
+ * using `React.createClass`, they will not be on the component's prototype.
363
+ */
364
+var RESERVED_SPEC_KEYS = {
365
+ // TODO: move this to RESERVED_STATIC_SPEC_KEYS, but this requires also
366
+ // changing the JSX transform.
367
+ displayName: function(ConvenienceConstructor, displayName) {
368
+ ConvenienceConstructor.componentConstructor.displayName = displayName;
369
+ },
370
+
371
+ statics: function(ConvenienceConstructor, statics) {
372
+ mixStaticSpecIntoComponent(ConvenienceConstructor, statics);
373
+ }
374
+};
375
+
376
function validateTypeDef(Constructor, typeDef, location) {
377
for (var propName in typeDef) {
378
if (typeDef.hasOwnProperty(propName)) {
@@ -360,11 +388,13 @@ function validateTypeDef(Constructor, typeDef, location) {
388
}
389
}
390
363
-function validateMethodOverride(proto, name) {
364
- var specPolicy = ReactCompositeComponentInterface[name];
391
+function validateMethodOverride(proto, name, isStatic) {
392
+ var specPolicy = isStatic ?
393
+ ReactCompositeComponentInterface.statics[name] :
394
+ ReactCompositeComponentInterface[name];
395
396
// Disallow overriding of base class methods unless explicitly allowed.
367
- if (ReactCompositeComponentMixin.hasOwnProperty(name)) {
397
+ if (!isStatic && ReactCompositeComponentMixin.hasOwnProperty(name)) {
398
invariant(
399
specPolicy === SpecPolicy.OVERRIDE_BASE,
400
'ReactCompositeComponentInterface: You are attempting to override ' +
@@ -387,7 +417,6 @@ function validateMethodOverride(proto, name) {
417
}
418
}
419
390
-
420
function validateLifeCycleOnReplaceState(instance) {
421
var compositeLifeCycleState = instance._compositeLifeCycleState;
422
invariant(
@@ -407,17 +436,27 @@ function validateLifeCycleOnReplaceState(instance) {
436
* Custom version of `mixInto` which handles policy validation and reserved
437
* specification keys when building `ReactCompositeComponent` classses.
438
*/
410
-function mixSpecIntoComponent(Constructor, spec) {
439
+function mixSpecIntoComponent(ConvenienceConstructor, spec) {
440
+ var Constructor = ConvenienceConstructor.componentConstructor;
441
var proto = Constructor.prototype;
442
for (var name in spec) {
443
var property = spec[name];
444
if (!spec.hasOwnProperty(name) || !property) {
445
continue;
446
}
417
- validateMethodOverride(proto, name);
447
+
448
+ validateMethodOverride(proto, name, false);
449
450
if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) {
420
- RESERVED_SPEC_KEYS[name](Constructor, property);
451
+ RESERVED_SPEC_KEYS[name](ConvenienceConstructor, property);
452
+ } else if (RESERVED_STATIC_SPEC_KEYS.hasOwnProperty(name)) {
453
+ if (__DEV__) {
454
+ console.warn(
455
+ 'createClass(...): `' + name + '` is now a static property and ' +
456
+ 'should be defined inside "statics".'
457
+ );
458
+ }
459
+ RESERVED_STATIC_SPEC_KEYS[name](ConvenienceConstructor, property);
460
} else {
461
// Setup methods on prototype:
462
// The following member methods should not be automatically bound:
@@ -457,6 +496,38 @@ function mixSpecIntoComponent(Constructor, spec) {
496
}
497
}
498
499
+function mixStaticSpecIntoComponent(ConvenienceConstructor, statics) {
500
+ if (!statics) {
501
+ return;
502
+ }
503
+ for (var name in statics) {
504
+ var property = statics[name];
505
+ if (!statics.hasOwnProperty(name) || !property) {
506
+ return;
507
+ }
508
+
509
+ validateMethodOverride(ConvenienceConstructor, name, true);
510
+
511
+ if (RESERVED_STATIC_SPEC_KEYS.hasOwnProperty(name)) {
512
+ RESERVED_STATIC_SPEC_KEYS[name](ConvenienceConstructor, property);
513
+ } else {
514
+ var isInherited = name in ConvenienceConstructor;
515
+ var result = property;
516
+ if (isInherited) {
517
+ var existingProperty = ConvenienceConstructor[name];
518
+ if (ReactCompositeComponentInterface.statics[name] ===
519
+ SpecPolicy.DEFINE_MANY_MERGED) {
520
+ result = createMergedResultFunction(existingProperty, property);
521
+ } else {
522
+ result = createChainedFunction(existingProperty, property);
523
+ }
524
+ }
525
+ ConvenienceConstructor[name] = result;
526
+ ConvenienceConstructor.componentConstructor[name] = result;
527
+ }
528
+ }
529
+}
530
+
531
/**
532
* Merge two objects, but throw if both contain the same key.
533
*
@@ -1172,7 +1243,16 @@ var ReactCompositeComponent = {
1243
var Constructor = function() {};
1244
Constructor.prototype = new ReactCompositeComponentBase();
1245
Constructor.prototype.constructor = Constructor;
1175
- mixSpecIntoComponent(Constructor, spec);
1246
+
1247
+ var ConvenienceConstructor = function(props, children) {
1248
+ var instance = new Constructor();
1249
+ instance.construct.apply(instance, arguments);
1250
+ return instance;
1251
+ };
1252
+ ConvenienceConstructor.componentConstructor = Constructor;
1253
+ ConvenienceConstructor.originalSpec = spec;
1254
+
1255
+ mixSpecIntoComponent(ConvenienceConstructor, spec);
1256
1257
invariant(
1258
Constructor.prototype.render,
@@ -1197,13 +1277,6 @@ var ReactCompositeComponent = {
1277
}
1278
}
1279
1200
- var ConvenienceConstructor = function(props, children) {
1201
- var instance = new Constructor();
1202
- instance.construct.apply(instance, arguments);
1203
- return instance;
1204
- };
1205
- ConvenienceConstructor.componentConstructor = Constructor;
1206
- ConvenienceConstructor.originalSpec = spec;
1280
return ConvenienceConstructor;
1281
},
1282
src/core/__tests__/ReactCompositeComponent-test.js
+179
-37
@@ -235,7 +235,9 @@ describe('ReactCompositeComponent', function() {
235
236
it('should normalize props with default values', function() {
237
var Component = React.createClass({
238
- propTypes: {key: ReactPropTypes.string.isRequired},
238
+ statics: {
239
+ propTypes: {key: ReactPropTypes.string.isRequired}
240
+ },
241
getDefaultProps: function() {
242
return {key: 'testKey'};
243
},
@@ -262,7 +264,9 @@ describe('ReactCompositeComponent', function() {
264
265
it('should check default prop values', function() {
266
var Component = React.createClass({
265
- propTypes: {key: ReactPropTypes.string.isRequired},
267
+ statics: {
268
+ propTypes: {key: ReactPropTypes.string.isRequired}
269
+ },
270
getDefaultProps: function() {
271
return {key: null};
272
},
@@ -282,8 +286,10 @@ describe('ReactCompositeComponent', function() {
286
287
it('should check declared prop types', function() {
288
var Component = React.createClass({
285
- propTypes: {
286
- key: ReactPropTypes.string.isRequired
289
+ statics: {
290
+ propTypes: {
291
+ key: ReactPropTypes.string.isRequired
292
+ }
293
},
294
render: function() {
295
return <span>{this.props.key}</span>;
@@ -313,8 +319,10 @@ describe('ReactCompositeComponent', function() {
319
expect(function() {
320
React.createClass({
321
displayName: 'Component',
316
- propTypes: {
317
- key: null
322
+ statics: {
323
+ propTypes: {
324
+ key: null
325
+ }
326
},
327
render: function() {
328
return <span>{this.props.key}</span>;
@@ -330,8 +338,10 @@ describe('ReactCompositeComponent', function() {
338
expect(function() {
339
React.createClass({
340
displayName: 'Component',
333
- contextTypes: {
334
- key: null
341
+ statics: {
342
+ contextTypes: {
343
+ key: null
344
+ }
345
},
346
render: function() {
347
return <span>{this.props.key}</span>;
@@ -347,8 +357,10 @@ describe('ReactCompositeComponent', function() {
357
expect(function() {
358
React.createClass({
359
displayName: 'Component',
350
- childContextTypes: {
351
- key: null
360
+ statics: {
361
+ childContextTypes: {
362
+ key: null
363
+ }
364
},
365
render: function() {
366
return <span>{this.props.key}</span>;
@@ -416,7 +428,9 @@ describe('ReactCompositeComponent', function() {
428
}
429
};
430
var Component = React.createClass({
419
- mixins: [Mixin],
431
+ statics: {
432
+ mixins: [Mixin]
433
+ },
434
getInitialState: function() {
435
return {component: true};
436
},
@@ -437,7 +451,9 @@ describe('ReactCompositeComponent', function() {
451
}
452
};
453
var Component = React.createClass({
440
- mixins: [Mixin],
454
+ statics: {
455
+ mixins: [Mixin]
456
+ },
457
getInitialState: function() {
458
return {x: true};
459
},
@@ -461,7 +477,9 @@ describe('ReactCompositeComponent', function() {
477
}
478
};
479
var Component = React.createClass({
464
- mixins: [Mixin],
480
+ statics: {
481
+ mixins: [Mixin]
482
+ },
483
getInitialState: function() {
484
return {x: true};
485
},
@@ -548,14 +566,59 @@ describe('ReactCompositeComponent', function() {
566
}
567
});
568
569
+ it('should warn when using deprecated non-static spec keys', function() {
570
+ var warn = console.warn;
571
+ console.warn = mocks.getMockFunction();
572
+ try {
573
+ React.createClass({
574
+ mixins: [{}],
575
+ propTypes: {
576
+ foo: ReactPropTypes.string
577
+ },
578
+ contextTypes: {
579
+ foo: ReactPropTypes.string
580
+ },
581
+ childContextTypes: {
582
+ foo: ReactPropTypes.string
583
+ },
584
+ render: function() {
585
+ return <div />;
586
+ }
587
+ });
588
+ expect(console.warn.mock.calls.length).toBe(4);
589
+ expect(console.warn.mock.calls[0][0]).toBe(
590
+ 'createClass(...): `mixins` is now a static property and should ' +
591
+ 'be defined inside "statics".'
592
+ );
593
+ expect(console.warn.mock.calls[1][0]).toBe(
594
+ 'createClass(...): `propTypes` is now a static property and should ' +
595
+ 'be defined inside "statics".'
596
+ );
597
+ expect(console.warn.mock.calls[2][0]).toBe(
598
+ 'createClass(...): `contextTypes` is now a static property and ' +
599
+ 'should be defined inside "statics".'
600
+ );
601
+ expect(console.warn.mock.calls[3][0]).toBe(
602
+ 'createClass(...): `childContextTypes` is now a static property and ' +
603
+ 'should be defined inside "statics".'
604
+ );
605
+ } catch (e) {
606
+ throw e;
607
+ } finally {
608
+ console.warn = warn;
609
+ }
610
+ });
611
+
612
it('should pass context', function() {
613
var childInstance = null;
614
var grandchildInstance = null;
615
616
var Parent = React.createClass({
556
- childContextTypes: {
557
- foo: ReactPropTypes.string,
558
- depth: ReactPropTypes.number
617
+ statics: {
618
+ childContextTypes: {
619
+ foo: ReactPropTypes.string,
620
+ depth: ReactPropTypes.number
621
+ }
622
},
623
624
getChildContext: function() {
@@ -572,13 +635,15 @@ describe('ReactCompositeComponent', function() {
635
});
636
637
var Child = React.createClass({
575
- contextTypes: {
576
- foo: ReactPropTypes.string,
577
- depth: ReactPropTypes.number
578
- },
638
+ statics: {
639
+ contextTypes: {
640
+ foo: ReactPropTypes.string,
641
+ depth: ReactPropTypes.number
642
+ },
643
580
- childContextTypes: {
581
- depth: ReactPropTypes.number
644
+ childContextTypes: {
645
+ depth: ReactPropTypes.number
646
+ }
647
},
648
649
getChildContext: function() {
@@ -594,9 +659,11 @@ describe('ReactCompositeComponent', function() {
659
});
660
661
var Grandchild = React.createClass({
597
- contextTypes: {
598
- foo: ReactPropTypes.string,
599
- depth: ReactPropTypes.number
662
+ statics: {
663
+ contextTypes: {
664
+ foo: ReactPropTypes.string,
665
+ depth: ReactPropTypes.number
666
+ }
667
},
668
669
render: function() {
@@ -612,8 +679,10 @@ describe('ReactCompositeComponent', function() {
679
680
it('should check context types', function() {
681
var Component = React.createClass({
615
- contextTypes: {
616
- foo: ReactPropTypes.string.isRequired
682
+ statics: {
683
+ contextTypes: {
684
+ foo: ReactPropTypes.string.isRequired
685
+ }
686
},
687
688
render: function() {
@@ -646,9 +715,11 @@ describe('ReactCompositeComponent', function() {
715
716
it('should check child context types', function() {
717
var Component = React.createClass({
649
- childContextTypes: {
650
- foo: ReactPropTypes.string.isRequired,
651
- bar: ReactPropTypes.number
718
+ statics: {
719
+ childContextTypes: {
720
+ foo: ReactPropTypes.string.isRequired,
721
+ bar: ReactPropTypes.number
722
+ }
723
},
724
725
getChildContext: function() {
@@ -693,8 +764,10 @@ describe('ReactCompositeComponent', function() {
764
765
it('should filter out context not in contextTypes', function() {
766
var Component = React.createClass({
696
- contextTypes: {
697
- foo: ReactPropTypes.string
767
+ statics: {
768
+ contextTypes: {
769
+ foo: ReactPropTypes.string
770
+ }
771
},
772
773
render: function() {
@@ -716,9 +789,11 @@ describe('ReactCompositeComponent', function() {
789
var actualComponentDidUpdate;
790
791
var Parent = React.createClass({
719
- childContextTypes: {
720
- foo: ReactPropTypes.string.isRequired,
721
- bar: ReactPropTypes.string.isRequired
792
+ statics: {
793
+ childContextTypes: {
794
+ foo: ReactPropTypes.string.isRequired,
795
+ bar: ReactPropTypes.string.isRequired
796
+ }
797
},
798
799
getChildContext: function() {
@@ -734,8 +809,10 @@ describe('ReactCompositeComponent', function() {
809
});
810
811
var Component = React.createClass({
737
- contextTypes: {
738
- foo: ReactPropTypes.string
812
+ statics: {
813
+ contextTypes: {
814
+ foo: ReactPropTypes.string
815
+ }
816
},
817
818
componentWillReceiveProps: function(nextProps, nextContext) {
@@ -769,4 +846,69 @@ describe('ReactCompositeComponent', function() {
846
expect(actualComponentWillUpdate).toEqual({foo: 'def'});
847
expect(actualComponentDidUpdate).toEqual({foo: 'abc'});
848
});
849
+
850
+ it('should support statics', function() {
851
+ var Component = React.createClass({
852
+ statics: {
853
+ abc: 'def'
854
+ },
855
+
856
+ render: function() {
857
+ return <span />;
858
+ }
859
+ });
860
+ var instance = <Component />;
861
+ ReactTestUtils.renderIntoDocument(instance);
862
+ expect(instance.constructor.abc).toBe('def');
863
+ expect(Component.abc).toBe('def');
864
+ });
865
+
866
+ it('should support statics in mixins', function() {
867
+ var Mixin = {
868
+ statics: {
869
+ foo: 'bar'
870
+ }
871
+ };
872
+ var Component = React.createClass({
873
+ mixins: [Mixin],
874
+
875
+ statics: {
876
+ abc: 'def'
877
+ },
878
+
879
+ render: function() {
880
+ return <span />;
881
+ }
882
+ });
883
+ var instance = <Component />;
884
+ ReactTestUtils.renderIntoDocument(instance);
885
+ expect(instance.constructor.foo).toBe('bar');
886
+ expect(Component.foo).toBe('bar');
887
+ expect(instance.constructor.abc).toBe('def');
888
+ expect(Component.abc).toBe('def');
889
+ });
890
+
891
+ it("should throw if mixins override each others' statics", function() {
892
+ expect(function() {
893
+ var Mixin = {
894
+ statics: {
895
+ abc: 'foo'
896
+ }
897
+ };
898
+ React.createClass({
899
+ statics: {
900
+ mixins: [Mixin],
901
+ abc: 'bar'
902
+ },
903
+
904
+ render: function() {
905
+ return <span />;
906
+ }
907
+ });
908
+ }).toThrow(
909
+ 'Invariant Violation: ReactCompositeComponentInterface: You are ' +
910
+ 'attempting to define `abc` on your component more than once. This ' +
911
+ 'conflict may be due to a mixin.'
912
+ );
913
+ });
914
});