@samitouri / QOS-React-2 / commits / 766a79c695

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);