Move component class instantiation into ReactCompositeComponent
We need to move instantiation into the mount phase for context purposes. To do this I moved the shallow rendering stuff into ReactTestUtils and reused more of the existing code for it by instantiating a noop child. Everywhere we refer to the "type" we should pass it to ReactNativeComponent to resolve any string value into the underlying composite.
Sebastian Markbage committed
Jan 22, 2015 at 18:16 UTC
766a79c6957ddc3b7040fbbb748df48ac0fcb625
5 files changed
+139
-167
src/classic/element/ReactElementValidator.js
+15
-7
@@ -22,6 +22,7 @@ var ReactElement = require('ReactElement');
22
var ReactPropTypeLocations = require('ReactPropTypeLocations');
23
var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
24
var ReactCurrentOwner = require('ReactCurrentOwner');
25
+var ReactNativeComponent = require('ReactNativeComponent');
26
27
var getIteratorFn = require('getIteratorFn');
28
var monitorCodeUse = require('monitorCodeUse');
@@ -361,26 +362,33 @@ var ReactElementValidator = {
362
}
363
364
if (type) {
364
- var name = type.displayName || type.name;
365
- if (type.propTypes) {
365
+ // Extract the component class from the element. Converts string types
366
+ // to a composite class which may have propTypes.
367
+ // TODO: Validating a string's propTypes is not decoupled from the
368
+ // rendering target which is problematic.
369
+ var componentClass = ReactNativeComponent.getComponentClassForElement(
370
+ element
371
+ );
372
+ var name = componentClass.displayName || componentClass.name;
373
+ if (componentClass.propTypes) {
374
checkPropTypes(
375
name,
368
- type.propTypes,
376
+ componentClass.propTypes,
377
element.props,
378
ReactPropTypeLocations.prop
379
);
380
}
373
- if (type.contextTypes) {
381
+ if (componentClass.contextTypes) {
382
checkPropTypes(
383
name,
376
- type.contextTypes,
384
+ componentClass.contextTypes,
385
element._context,
386
ReactPropTypeLocations.context
387
);
388
}
381
- if (typeof type.getDefaultProps === 'function') {
389
+ if (typeof componentClass.getDefaultProps === 'function') {
390
warning(
383
- type.getDefaultProps.isReactClassApproved,
391
+ componentClass.getDefaultProps.isReactClassApproved,
392
'getDefaultProps is only used on classic React.createClass ' +
393
'definitions. Use a static property named `defaultProps` instead.'
394
);
src/core/ReactCompositeComponent.js
+56
-120
@@ -17,6 +17,7 @@ var ReactContext = require('ReactContext');
17
var ReactCurrentOwner = require('ReactCurrentOwner');
18
var ReactElement = require('ReactElement');
19
var ReactInstanceMap = require('ReactInstanceMap');
20
+var ReactNativeComponent = require('ReactNativeComponent');
21
var ReactPerf = require('ReactPerf');
22
var ReactPropTypeLocations = require('ReactPropTypeLocations');
23
var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
@@ -108,12 +109,7 @@ var ReactCompositeComponentMixin = assign({},
109
*/
110
construct: function(element) {
111
this._rootNodeID = null;
111
-
112
- this._instance.props = element.props;
113
- // instance.state get set up to its proper initial value in mount
114
- // which may be null.
115
- this._instance.context = null;
116
- this._instance.refs = emptyObject;
112
+ this._instance = null;
113
114
this._pendingElement = null;
115
this._pendingContext = null;
@@ -165,18 +161,31 @@ var ReactCompositeComponentMixin = assign({},
161
this._mountOrder = nextMountID++;
162
this._rootNodeID = rootID;
163
168
- var inst = this._instance;
164
+ var publicProps = this._processProps(this._currentElement.props);
165
+ var publicContext = this._processContext(this._currentElement._context);
166
+
167
+ var Component = ReactNativeComponent.getComponentClassForElement(
168
+ this._currentElement
169
+ );
170
+
171
+ // Initialize the public class
172
+ var inst = new Component(publicProps);
173
+ // These should be set up in the constructor, but as a convenience for
174
+ // simpler class abstractions, we set them up after the fact.
175
+ inst.props = publicProps;
176
+ inst.context = publicContext;
177
+ inst.refs = emptyObject;
178
+
179
+ this._instance = inst;
180
181
// Store a reference from the instance back to the internal representation
182
ReactInstanceMap.set(inst, this);
183
184
this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;
185
175
- inst.context = this._processContext(this._currentElement._context);
186
if (__DEV__) {
187
this._warnIfContextsDiffer(this._currentElement._context, context);
188
}
179
- inst.props = this._processProps(this._currentElement.props);
189
190
if (__DEV__) {
191
// Since plain JS classes are defined without any special initialization
@@ -485,7 +494,12 @@ var ReactCompositeComponentMixin = assign({},
494
*/
495
_maskContext: function(context) {
496
var maskedContext = null;
488
- var contextTypes = this._instance.constructor.contextTypes;
497
+ // This really should be getting the component class for the element,
498
+ // but we know that we're not going to need it for built-ins.
499
+ if (typeof this._currentElement.type === 'string') {
500
+ return emptyObject;
501
+ }
502
+ var contextTypes = this._currentElement.type.contextTypes;
503
if (!contextTypes) {
504
return emptyObject;
505
}
@@ -506,16 +520,17 @@ var ReactCompositeComponentMixin = assign({},
520
*/
521
_processContext: function(context) {
522
var maskedContext = this._maskContext(context);
509
- var contextTypes = this._instance.constructor.contextTypes;
510
- if (!contextTypes) {
511
- return emptyObject;
512
- }
523
if (__DEV__) {
514
- this._checkPropTypes(
515
- contextTypes,
516
- maskedContext,
517
- ReactPropTypeLocations.context
524
+ var Component = ReactNativeComponent.getComponentClassForElement(
525
+ this._currentElement
526
);
527
+ if (Component.contextTypes) {
528
+ this._checkPropTypes(
529
+ Component.contextTypes,
530
+ maskedContext,
531
+ ReactPropTypeLocations.context
532
+ );
533
+ }
534
}
535
return maskedContext;
536
},
@@ -566,10 +581,15 @@ var ReactCompositeComponentMixin = assign({},
581
*/
582
_processProps: function(newProps) {
583
if (__DEV__) {
569
- var inst = this._instance;
570
- var propTypes = inst.constructor.propTypes;
571
- if (propTypes) {
572
- this._checkPropTypes(propTypes, newProps, ReactPropTypeLocations.prop);
584
+ var Component = ReactNativeComponent.getComponentClassForElement(
585
+ this._currentElement
586
+ );
587
+ if (Component.propTypes) {
588
+ this._checkPropTypes(
589
+ Component.propTypes,
590
+ newProps,
591
+ ReactPropTypeLocations.prop
592
+ );
593
}
594
}
595
return newProps;
@@ -894,15 +914,22 @@ var ReactCompositeComponentMixin = assign({},
914
transaction,
915
context
916
);
897
- ReactComponentEnvironment.replaceNodeWithMarkupByID(
898
- prevComponentID,
899
- nextMarkup
900
- );
917
+ this._replaceNodeWithMarkupByID(prevComponentID, nextMarkup);
918
}
919
},
920
921
/**
905
- * @private
922
+ * @protected
923
+ */
924
+ _replaceNodeWithMarkupByID: function(prevComponentID, nextMarkup) {
925
+ ReactComponentEnvironment.replaceNodeWithMarkupByID(
926
+ prevComponentID,
927
+ nextMarkup
928
+ );
929
+ },
930
+
931
+ /**
932
+ * @protected
933
*/
934
_renderValidatedComponentWithoutOwnerOrContext: function() {
935
var inst = this._instance;
@@ -983,7 +1010,7 @@ var ReactCompositeComponentMixin = assign({},
1010
*/
1011
getName: function() {
1012
var type = this._currentElement.type;
986
- var constructor = this._instance.constructor;
1013
+ var constructor = this._instance && this._instance.constructor;
1014
return (
1015
type.displayName || (constructor && constructor.displayName) ||
1016
type.name || (constructor && constructor.name) ||
@@ -1008,95 +1035,6 @@ var ReactCompositeComponentMixin = assign({},
1035
1036
});
1037
1011
-var ShallowMixin = assign({},
1012
- ReactCompositeComponentMixin, {
1013
-
1014
- /**
1015
- * Initializes the component, renders markup, and registers event listeners.
1016
- *
1017
- * @param {string} rootID DOM ID of the root node.
1018
- * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
1019
- * @return {ReactElement} Shallow rendering of the component.
1020
- * @final
1021
- * @internal
1022
- */
1023
- mountComponent: function(rootID, transaction, context) {
1024
- ReactComponent.Mixin.mountComponent.call(
1025
- this,
1026
- rootID,
1027
- transaction,
1028
- context
1029
- );
1030
-
1031
- var inst = this._instance;
1032
-
1033
- // Store a reference from the instance back to the internal representation
1034
- ReactInstanceMap.set(inst, this);
1035
-
1036
- this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;
1037
-
1038
- // No context for shallow-mounted components.
1039
- inst.props = this._processProps(this._currentElement.props);
1040
-
1041
- var initialState = inst.state;
1042
- if (initialState === undefined) {
1043
- inst.state = initialState = null;
1044
- }
1045
- invariant(
1046
- typeof initialState === 'object' && !Array.isArray(initialState),
1047
- '%s.state: must be set to an object or null',
1048
- this.getName() || 'ReactCompositeComponent'
1049
- );
1050
-
1051
- this._pendingState = null;
1052
- this._pendingForceUpdate = false;
1053
-
1054
- if (inst.componentWillMount) {
1055
- inst.componentWillMount();
1056
- // When mounting, calls to `setState` by `componentWillMount` will set
1057
- // `this._pendingState` without triggering a re-render.
1058
- if (this._pendingState) {
1059
- inst.state = this._pendingState;
1060
- this._pendingState = null;
1061
- }
1062
- }
1063
-
1064
- // No recursive call to instantiateReactComponent for shallow rendering.
1065
- this._renderedComponent =
1066
- this._renderValidatedComponentWithoutOwnerOrContext();
1067
-
1068
- // Done with mounting, `setState` will now trigger UI changes.
1069
- this._compositeLifeCycleState = null;
1070
-
1071
- // No call to this._renderedComponent.mountComponent for shallow
1072
- // rendering.
1073
-
1074
- if (inst.componentDidMount) {
1075
- transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
1076
- }
1077
-
1078
- return this._renderedComponent;
1079
- },
1080
-
1081
- /**
1082
- * Call the component's `render` method and update the DOM accordingly.
1083
- *
1084
- * @param {ReactReconcileTransaction} transaction
1085
- * @internal
1086
- */
1087
- _updateRenderedComponent: function(transaction) {
1088
- var prevComponentInstance = this._renderedComponent;
1089
- var prevRenderedElement = prevComponentInstance._currentElement;
1090
- // Use the without-owner-or-context variant of _rVC below:
1091
- var nextRenderedElement =
1092
- this._renderValidatedComponentWithoutOwnerOrContext();
1093
- // This is a noop in shallow render
1094
- shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement);
1095
- this._renderedComponent = nextRenderedElement;
1096
- }
1097
-
1098
-});
1099
-
1038
ReactPerf.measureMethods(
1039
ReactCompositeComponentMixin,
1040
'ReactCompositeComponent',
@@ -1111,9 +1049,7 @@ var ReactCompositeComponent = {
1049
1050
LifeCycle: CompositeLifeCycle,
1051
1114
- Mixin: ReactCompositeComponentMixin,
1115
-
1116
- ShallowMixin: ShallowMixin
1052
+ Mixin: ReactCompositeComponentMixin
1053
1054
};
1055
src/core/ReactNativeComponent.js
+26
-16
@@ -57,27 +57,36 @@ function autoGenerateWrapperClass(type) {
57
}
58
59
/**
60
- * Create an internal class for a specific tag.
60
+ * Get a composite component wrapper class for a specific tag.
61
*
62
- * @param {string} tag The tag for which to create an internal instance.
63
- * @param {any} props The props passed to the instance constructor.
64
- * @return {ReactComponent} component The injected empty component.
62
+ * @param {ReactElement} element The tag for which to get the class.
63
+ * @return {function} The React class constructor function.
64
*/
66
-function createInstanceForTag(tag, props, parentType) {
65
+function getComponentClassForElement(element) {
66
+ if (typeof element.type === 'function') {
67
+ return element.type;
68
+ }
69
+ var tag = element.type;
70
var componentClass = tagToComponentClass[tag];
71
if (componentClass == null) {
72
tagToComponentClass[tag] = componentClass = autoGenerateWrapperClass(tag);
73
}
71
- if (parentType === tag) {
72
- // Avoid recursion
73
- invariant(
74
- genericComponentClass,
75
- 'There is no registered component for the tag %s',
76
- tag
77
- );
78
- return new genericComponentClass(tag, props);
79
- }
80
- return new componentClass(props);
74
+ return componentClass;
75
+}
76
+
77
+/**
78
+ * Get a native internal component class for a specific tag.
79
+ *
80
+ * @param {ReactElement} element The element to create.
81
+ * @return {function} The internal class constructor function.
82
+ */
83
+function createInternalComponent(element) {
84
+ invariant(
85
+ genericComponentClass,
86
+ 'There is no registered component for the tag %s',
87
+ element.type
88
+ );
89
+ return new genericComponentClass(element.type, element.props);
90
}
91
92
/**
@@ -97,7 +106,8 @@ function isTextComponent(component) {
106
}
107
108
var ReactNativeComponent = {
100
- createInstanceForTag: createInstanceForTag,
109
+ getComponentClassForElement: getComponentClassForElement,
110
+ createInternalComponent: createInternalComponent,
111
createInstanceForText: createInstanceForText,
112
isTextComponent: isTextComponent,
113
injection: ReactNativeComponentInjection
src/core/instantiateReactComponent.js
+8
-19
@@ -21,9 +21,7 @@ var invariant = require('invariant');
21
var warning = require('warning');
22
23
// To avoid a cyclic dependency, we create the final class in this module
24
-var ReactCompositeComponentWrapper = function(inst) {
25
- this._instance = inst;
26
-};
24
+var ReactCompositeComponentWrapper = function() { };
25
assign(
26
ReactCompositeComponentWrapper.prototype,
27
ReactCompositeComponent.Mixin,
@@ -73,28 +71,19 @@ function instantiateReactComponent(node, parentCompositeType) {
71
}
72
73
// Special case string values
76
- if (typeof element.type === 'string') {
77
- instance = ReactNativeComponent.createInstanceForTag(
78
- element.type,
79
- element.props,
80
- parentCompositeType
81
- );
82
- // If the injected special class is not an internal class, but another
83
- // composite, then we must wrap it.
84
- // TODO: Move this resolution around to something cleaner.
85
- if (typeof instance.mountComponent !== 'function') {
86
- instance = new ReactCompositeComponentWrapper(instance);
87
- }
74
+ if (parentCompositeType === element.type &&
75
+ typeof element.type === 'string') {
76
+ // Avoid recursion if the wrapper renders itself.
77
+ instance = ReactNativeComponent.createInternalComponent(element);
78
+ // All native components are currently wrapped in a composite so we're
79
+ // safe to assume that this is what we should instantiate.
80
} else if (isInternalComponentType(element.type)) {
81
// This is temporarily available for custom components that are not string
82
// represenations. I.e. ART. Once those are updated to use the string
83
// representation, we can drop this code path.
84
instance = new element.type(element);
85
} else {
94
- // TODO: Update to follow new ES6 initialization. Ideally, we can use
95
- // props in property initializers.
96
- var inst = new element.type(element.props);
97
- instance = new ReactCompositeComponentWrapper(inst);
86
+ instance = new ReactCompositeComponentWrapper();
87
}
88
} else if (typeof node === 'string' || typeof node === 'number') {
89
instance = ReactNativeComponent.createInstanceForText(node);
src/test/ReactTestUtils.js
+34
-5
@@ -321,15 +321,44 @@ var ReactShallowRenderer = function() {
321
};
322
323
ReactShallowRenderer.prototype.getRenderOutput = function() {
324
- return (this._instance && this._instance._renderedComponent) || null;
324
+ return (
325
+ (this._instance && this._instance._renderedComponent &&
326
+ this._instance._renderedComponent._currentElement)
327
+ || null
328
+ );
329
};
330
327
-var ShallowComponentWrapper = function(inst) {
328
- this._instance = inst;
331
+var NoopInternalComponent = function(element) {
332
+ this._currentElement = element;
333
+}
334
+
335
+NoopInternalComponent.prototype = {
336
+
337
+ mountComponent: function() {
338
+ },
339
+
340
+ receiveComponent: function(element) {
341
+ this._currentElement = element;
342
+ },
343
+
344
+ unmountComponent: function() {
345
+
346
+ }
347
+
348
};
349
+
350
+var ShallowComponentWrapper = function() { };
351
assign(
352
ShallowComponentWrapper.prototype,
332
- ReactCompositeComponent.ShallowMixin
353
+ ReactCompositeComponent.Mixin, {
354
+ _instantiateReactComponent: function(element) {
355
+ return new NoopInternalComponent(element);
356
+ },
357
+ _replaceNodeWithMarkupByID: function() {},
358
+ _renderValidatedComponent:
359
+ ReactCompositeComponent.Mixin.
360
+ _renderValidatedComponentWithoutOwnerOrContext
361
+ }
362
);
363
364
ReactShallowRenderer.prototype.render = function(element, context) {
@@ -341,7 +370,7 @@ ReactShallowRenderer.prototype.render = function(element, context) {
370
ReactShallowRenderer.prototype._render = function(element, transaction, context) {
371
if (!this._instance) {
372
var rootID = ReactInstanceHandles.createReactRootID();
344
- var instance = new ShallowComponentWrapper(new element.type(element.props));
373
+ var instance = new ShallowComponentWrapper(element.type);
374
instance.construct(element);
375
376
instance.mountComponent(rootID, transaction, context);