@samitouri / QOS-React-2 / commits / 5aab0bddaa

Move key/ref off props and prepare for new descriptor factories

Breaking changes - key/ref are no longer accessible on props but they are accessible on the descriptors. This means that parents/owners can access it but not the component itself. - Descriptor factories are now plain functions and you can't rely on the prototype or constructors of descriptors to identify the component type. Existing descriptor factories are now wrapped in a legacy factory. Currently it does nothing but it will give us a hook to track callers to factories that are not using JSX but just invoking the function directly. It also proxies static methods/properties to the underlying class. The newer factories don't have this feature. ReactTextComponent has it's own little factory because it's props is not an object. This is a detail and will go away once ReactTextComponent no longer needs descriptors.

Sebastian Markbage committed Jul 8, 2014 at 14:27 UTC 5aab0bddaa9dd2684049194a2488d57517d89cae
16 files changed +534 -294
src/browser/ReactDOM.js
+6 -6
@@ -21,6 +21,7 @@
21
22 var ReactDescriptor = require('ReactDescriptor');
23 var ReactDescriptorValidator = require('ReactDescriptorValidator');
24 +var ReactLegacyDescriptor = require('ReactLegacyDescriptor');
25 var ReactDOMComponent = require('ReactDOMComponent');
26
27 var mergeInto = require('mergeInto');
@@ -49,15 +50,14 @@ function createDOMComponentClass(omitClose, tag) {
50 Constructor.prototype.constructor = Constructor;
51 Constructor.displayName = tag;
52
52 - var ConvenienceConstructor = ReactDescriptor.createFactory(Constructor);
53 -
53 if (__DEV__) {
55 - return ReactDescriptorValidator.createFactory(
56 - ConvenienceConstructor
54 + return ReactLegacyDescriptor.wrapFactory(
55 + ReactDescriptorValidator.createFactory(Constructor)
56 );
57 }
59 -
60 - return ConvenienceConstructor;
58 + return ReactLegacyDescriptor.wrapFactory(
59 + ReactDescriptor.createFactory(Constructor)
60 + );
61 }
62
63 /**
src/browser/ReactTextComponent.js
+8 -1
@@ -104,4 +104,11 @@ mixInto(ReactTextComponent, {
104
105 });
106
107 -module.exports = ReactDescriptor.createFactory(ReactTextComponent);
107 +var ReactTextComponentFactory = function(text) {
108 + // Bypass validation and configuration
109 + return new ReactDescriptor(ReactTextComponent, null, null, null, null, text);
110 +};
111 +
112 +ReactTextComponentFactory.type = ReactTextComponent;
113 +
114 +module.exports = ReactTextComponentFactory;
src/browser/ui/React.js
+11 -4
@@ -26,6 +26,7 @@ var ReactCompositeComponent = require('ReactCompositeComponent');
26 var ReactContext = require('ReactContext');
27 var ReactCurrentOwner = require('ReactCurrentOwner');
28 var ReactDescriptor = require('ReactDescriptor');
29 +var ReactDescriptorValidator = require('ReactDescriptorValidator');
30 var ReactDOM = require('ReactDOM');
31 var ReactDOMComponent = require('ReactDOMComponent');
32 var ReactDefaultInjection = require('ReactDefaultInjection');
@@ -41,6 +42,14 @@ var onlyChild = require('onlyChild');
42
43 ReactDefaultInjection.inject();
44
45 +var createDescriptor = ReactDescriptor.createDescriptor;
46 +var createFactory = ReactDescriptor.createFactory;
47 +
48 +if (__DEV__) {
49 + createDescriptor = ReactDescriptorValidator.createDescriptor;
50 + createFactory = ReactDescriptorValidator.createFactory;
51 +}
52 +
53 var React = {
54 Children: {
55 map: ReactChildren.map,
@@ -54,10 +63,8 @@ var React = {
63 EventPluginUtils.useTouchEvents = shouldUseTouch;
64 },
65 createClass: ReactCompositeComponent.createClass,
57 - createDescriptor: function(type, props, children) {
58 - var args = Array.prototype.slice.call(arguments, 1);
59 - return type.apply(null, args);
60 - },
66 + createDescriptor: createDescriptor,
67 + createFactory: createFactory,
68 constructAndRenderComponent: ReactMount.constructAndRenderComponent,
69 constructAndRenderComponentByID: ReactMount.constructAndRenderComponentByID,
70 renderComponent: ReactPerf.measure(
src/core/ReactComponent.js
+11 -11
@@ -259,10 +259,10 @@ var ReactComponent = {
259 'single component instance in multiple places.',
260 rootID
261 );
262 - var props = this._descriptor.props;
263 - if (props.ref != null) {
262 + var ref = this._descriptor.ref;
263 + if (ref != null) {
264 var owner = this._descriptor._owner;
265 - ReactOwner.addComponentAsRefTo(this, props.ref, owner);
265 + ReactOwner.addComponentAsRefTo(this, ref, owner);
266 }
267 this._rootNodeID = rootID;
268 this._lifeCycleState = ComponentLifeCycle.MOUNTED;
@@ -285,9 +285,9 @@ var ReactComponent = {
285 this.isMounted(),
286 'unmountComponent(): Can only unmount a mounted component.'
287 );
288 - var props = this.props;
289 - if (props.ref != null) {
290 - ReactOwner.removeComponentAsRefFrom(this, props.ref, this._owner);
288 + var ref = this._descriptor.ref;
289 + if (ref != null) {
290 + ReactOwner.removeComponentAsRefFrom(this, ref, this._owner);
291 }
292 unmountIDFromEnvironment(this._rootNodeID);
293 this._rootNodeID = null;
@@ -356,17 +356,17 @@ var ReactComponent = {
356 // instantiateReactComponent is done.
357
358 if (nextDescriptor._owner !== prevDescriptor._owner ||
359 - nextDescriptor.props.ref !== prevDescriptor.props.ref) {
360 - if (prevDescriptor.props.ref != null) {
359 + nextDescriptor.ref !== prevDescriptor.ref) {
360 + if (prevDescriptor.ref != null) {
361 ReactOwner.removeComponentAsRefFrom(
362 - this, prevDescriptor.props.ref, prevDescriptor._owner
362 + this, prevDescriptor.ref, prevDescriptor._owner
363 );
364 }
365 // Correct, even if the owner is the same, and only the ref has changed.
366 - if (nextDescriptor.props.ref != null) {
366 + if (nextDescriptor.ref != null) {
367 ReactOwner.addComponentAsRefTo(
368 this,
369 - nextDescriptor.props.ref,
369 + nextDescriptor.ref,
370 nextDescriptor._owner
371 );
372 }
src/core/ReactCompositeComponent.js
+6 -8
@@ -25,6 +25,7 @@ var ReactDescriptor = require('ReactDescriptor');
25 var ReactDescriptorValidator = require('ReactDescriptorValidator');
26 var ReactEmptyComponent = require('ReactEmptyComponent');
27 var ReactErrorUtils = require('ReactErrorUtils');
28 +var ReactLegacyDescriptor = require('ReactLegacyDescriptor');
29 var ReactOwner = require('ReactOwner');
30 var ReactPerf = require('ReactPerf');
31 var ReactPropTransferer = require('ReactPropTransferer');
@@ -1387,17 +1388,14 @@ var ReactCompositeComponent = {
1388 }
1389 }
1390
1390 - var descriptorFactory = ReactDescriptor.createFactory(Constructor);
1391 -
1391 if (__DEV__) {
1393 - return ReactDescriptorValidator.createFactory(
1394 - descriptorFactory,
1395 - Constructor.propTypes,
1396 - Constructor.contextTypes
1392 + return ReactLegacyDescriptor.wrapFactory(
1393 + ReactDescriptorValidator.createFactory(Constructor)
1394 );
1395 }
1399 -
1400 - return descriptorFactory;
1396 + return ReactLegacyDescriptor.wrapFactory(
1397 + ReactDescriptor.createFactory(Constructor)
1398 + );
1399 },
1400
1401 injection: {
src/core/ReactDescriptor.js
+124 -115
@@ -21,9 +21,13 @@
21 var ReactContext = require('ReactContext');
22 var ReactCurrentOwner = require('ReactCurrentOwner');
23
24 -var merge = require('merge');
24 var warning = require('warning');
25
26 +var RESERVED_PROPS = {
27 + key: true,
28 + ref: true
29 +};
30 +
31 /**
32 * Warn for mutations.
33 *
@@ -81,149 +85,152 @@ function defineMutationMembrane(prototype) {
85 }
86 }
87
84 -/**
85 - * Transfer static properties from the source to the target. Functions are
86 - * rebound to have this reflect the original source.
87 - */
88 -function proxyStaticMethods(target, source) {
89 - if (typeof source !== 'function') {
90 - return;
91 - }
92 - for (var key in source) {
93 - if (source.hasOwnProperty(key)) {
94 - var value = source[key];
95 - if (typeof value === 'function') {
96 - var bound = value.bind(source);
97 - // Copy any properties defined on the function, such as `isRequired` on
98 - // a PropTypes validator. (mergeInto refuses to work on functions.)
99 - for (var k in value) {
100 - if (value.hasOwnProperty(k)) {
101 - bound[k] = value[k];
102 - }
103 - }
104 - target[key] = bound;
105 - } else {
106 - target[key] = value;
107 - }
108 - }
109 - }
110 -}
111 -
88 /**
89 * Base constructor for all React descriptors. This is only used to make this
90 * work with a dynamic instanceof check. Nothing should live on this prototype.
91 *
92 * @param {*} type
93 + * @param {string|object} ref
94 + * @param {*} key
95 + * @params {*} props
96 * @internal
97 */
119 -var ReactDescriptor = function() {};
98 +var ReactDescriptor = function(type, key, ref, owner, context, props) {
99 + // Built-in properties that belong on the descriptor
100 + this.type = type;
101 + this.key = key;
102 + this.ref = ref;
103 +
104 + // Record the component responsible for creating this descriptor.
105 + this._owner = owner;
106 +
107 + // TODO: Deprecate withContext, and then the context becomes accessible
108 + // through the owner.
109 + this._context = context;
110 +
111 + if (__DEV__) {
112 + // The validation flag and props are currently mutative. We put them on
113 + // an external backing store so that we can freeze the whole object.
114 + // This can be replaced with a WeakMap once they are implemented in
115 + // commonly used development environments.
116 + this._store = { validated: false, props: props };
117 +
118 + // We're not allowed to set props directly on the object so we early
119 + // return and rely on the prototype membrane to forward to the backing
120 + // store.
121 + if (useMutationMembrane) {
122 + Object.freeze(this);
123 + return;
124 + }
125 + }
126 +
127 + this.props = props;
128 +};
129
130 if (__DEV__) {
131 defineMutationMembrane(ReactDescriptor.prototype);
132 }
133
125 -ReactDescriptor.createFactory = function(type) {
126 -
127 - var descriptorPrototype = Object.create(ReactDescriptor.prototype);
128 -
129 - var factory = function(props, children) {
130 - // For consistency we currently allocate a new object for every descriptor.
131 - // This protects the descriptor from being mutated by the original props
132 - // object being mutated. It also protects the original props object from
133 - // being mutated by children arguments and default props. This behavior
134 - // comes with a performance cost and could be deprecated in the future.
135 - // It could also be optimized with a smarter JSX transform.
136 - if (props == null) {
137 - props = {};
138 - } else if (typeof props === 'object') {
139 - props = merge(props);
140 - }
134 +ReactDescriptor.createDescriptor = function(type, config, children) {
135 + if (type.isReactLegacyFactory) {
136 + // This is probably a legacy factory created by ReactCompositeComponent.
137 + // We unwrap it to get to the underlying class.
138 + // TODO: Drop this check when we drop ReactLegacyDescriptor.
139 + type = type.type;
140 + }
141
142 - // Children can be more than one argument, and those are transferred onto
143 - // the newly allocated props object.
144 - var childrenLength = arguments.length - 1;
145 - if (childrenLength === 1) {
146 - props.children = children;
147 - } else if (childrenLength > 1) {
148 - var childArray = Array(childrenLength);
149 - for (var i = 0; i < childrenLength; i++) {
150 - childArray[i] = arguments[i + 1];
142 + if (typeof type === 'function') {
143 + var isReactClass =
144 + typeof type.prototype.mountComponent === 'function' &&
145 + typeof type.prototype.receiveComponent === 'function';
146 + if (!isReactClass) {
147 + // This is being called with a plain function we should invoke it
148 + // immediately as if this was used with legacy JSX.
149 + if (__DEV__) {
150 + warning(
151 + false,
152 + 'This JSX uses a plain function. Only React components are valid in' +
153 + 'JSX.'
154 + );
155 }
152 - props.children = childArray;
156 + var args = Array.prototype.slice.call(arguments, 1);
157 + return type.apply(null, args);
158 }
159 + }
160 +
161 + // Reserved names are extracted
162 + var props = {};
163
155 - // Initialize the descriptor object
156 - var descriptor = Object.create(descriptorPrototype);
157 -
158 - // Record the component responsible for creating this descriptor.
159 - descriptor._owner = ReactCurrentOwner.current;
160 -
161 - // TODO: Deprecate withContext, and then the context becomes accessible
162 - // through the owner.
163 - descriptor._context = ReactContext.current;
164 -
165 - if (__DEV__) {
166 - // The validation flag and props are currently mutative. We put them on
167 - // an external backing store so that we can freeze the whole object.
168 - // This can be replaced with a WeakMap once they are implemented in
169 - // commonly used development environments.
170 - descriptor._store = { validated: false, props: props };
171 -
172 - // We're not allowed to set props directly on the object so we early
173 - // return and rely on the prototype membrane to forward to the backing
174 - // store.
175 - if (useMutationMembrane) {
176 - Object.freeze(descriptor);
177 - return descriptor;
164 + var key = null;
165 + var ref = null;
166 +
167 + if (config != null) {
168 + ref = config.ref === undefined ? null : config.ref;
169 + key = config.key === undefined ? null : '' + config.key;
170 + // Remaining properties are added to a new props object
171 + for (var propName in config) {
172 + if (config.hasOwnProperty(propName) &&
173 + !RESERVED_PROPS.hasOwnProperty(propName)) {
174 + props[propName] = config[propName];
175 }
176 }
177 + }
178
181 - descriptor.props = props;
182 - return descriptor;
183 - };
179 + // TODO: fill in defaultProps here, after transferPropsTo is gone
180 +
181 + // Children can be more than one argument, and those are transferred onto
182 + // the newly allocated props object.
183 + var childrenLength = arguments.length - 2;
184 + if (childrenLength === 1) {
185 + props.children = children;
186 + } else if (childrenLength > 1) {
187 + var childArray = Array(childrenLength);
188 + for (var i = 0; i < childrenLength; i++) {
189 + childArray[i] = arguments[i + 2];
190 + }
191 + props.children = childArray;
192 + }
193
185 - // Currently we expose the prototype of the descriptor so that
186 - // <Foo /> instanceof Foo works. This is controversial pattern.
187 - factory.prototype = descriptorPrototype;
194 + return new ReactDescriptor(
195 + type,
196 + key,
197 + ref,
198 + ReactCurrentOwner.current,
199 + ReactContext.current,
200 + props
201 + );
202 +};
203
204 +ReactDescriptor.createFactory = function(type) {
205 + if (type.isReactLegacyFactory) {
206 + // This is probably a legacy factory created by ReactCompositeComponent.
207 + // We unwrap it to get to the underlying class.
208 + // TODO: Drop this check when we drop ReactLegacyDescriptor.
209 + type = type.type;
210 + }
211 + var factory = ReactDescriptor.createDescriptor.bind(null, type);
212 // Expose the type on the factory and the prototype so that it can be
190 - // easily accessed on descriptors. E.g. <Foo />.type === Foo.type and for
191 - // static methods like <Foo />.type.staticMethod();
192 - // This should not be named constructor since this may not be the function
213 + // easily accessed on descriptors. E.g. <Foo />.type === Foo.type.
214 + // This should not be named `constructor` since this may not be the function
215 // that created the descriptor, and it may not even be a constructor.
216 factory.type = type;
195 - descriptorPrototype.type = type;
196 -
197 - proxyStaticMethods(factory, type);
198 -
199 - // Expose a unique constructor on the prototype is that this works with type
200 - // systems that compare constructor properties: <Foo />.constructor === Foo
201 - // This may be controversial since it requires a known factory function.
202 - descriptorPrototype.constructor = factory;
203 -
217 return factory;
205 -
218 };
219
220 ReactDescriptor.cloneAndReplaceProps = function(oldDescriptor, newProps) {
209 - var newDescriptor = Object.create(oldDescriptor.constructor.prototype);
210 - // It's important that this property order matches the hidden class of the
211 - // original descriptor to maintain perf.
212 - newDescriptor._owner = oldDescriptor._owner;
213 - newDescriptor._context = oldDescriptor._context;
221 + var newDescriptor = new ReactDescriptor(
222 + oldDescriptor.type,
223 + oldDescriptor.key,
224 + oldDescriptor.ref,
225 + oldDescriptor._owner,
226 + oldDescriptor._context,
227 + newProps
228 + );
229
230 if (__DEV__) {
216 - newDescriptor._store = {
217 - validated: oldDescriptor._store.validated,
218 - props: newProps
219 - };
220 - if (useMutationMembrane) {
221 - Object.freeze(newDescriptor);
222 - return newDescriptor;
223 - }
231 + // If the key on the original is valid, then the clone is valid
232 + newDescriptor._store.validated = oldDescriptor._store.validated;
233 }
225 -
226 - newDescriptor.props = newProps;
234 return newDescriptor;
235 };
236
@@ -235,8 +242,10 @@ ReactDescriptor.cloneAndReplaceProps = function(oldDescriptor, newProps) {
242 * @public
243 */
244 ReactDescriptor.isValidFactory = function(factory) {
238 - return typeof factory === 'function' &&
239 - factory.prototype instanceof ReactDescriptor;
245 + return typeof factory === 'function' &&
246 + typeof factory.type === 'function' &&
247 + typeof factory.type.prototype.mountComponent === 'function' &&
248 + typeof factory.type.prototype.receiveComponent === 'function';
249 };
250
251 /**
src/core/ReactDescriptorValidator.js
+36 -46
@@ -68,7 +68,7 @@ function getCurrentOwnerDisplayName() {
68 * @param {*} parentType component's parent's type.
69 */
70 function validateExplicitKey(component, parentType) {
71 - if (component._store.validated || component.props.key != null) {
71 + if (component._store.validated || component.key != null) {
72 return;
73 }
74 component._store.validated = true;
@@ -226,55 +226,45 @@ function checkPropTypes(componentName, propTypes, props, location) {
226
227 var ReactDescriptorValidator = {
228
229 - /**
230 - * Wraps a descriptor factory function in another function which validates
231 - * the props and context of the descriptor and warns about any failed type
232 - * checks.
233 - *
234 - * @param {function} factory The original descriptor factory
235 - * @param {object?} propTypes A prop type definition set
236 - * @param {object?} contextTypes A context type definition set
237 - * @return {object} The component descriptor, which may be invalid.
238 - * @private
239 - */
240 - createFactory: function(factory, propTypes, contextTypes) {
241 - var validatedFactory = function(props, children) {
242 - var descriptor = factory.apply(this, arguments);
229 + createDescriptor: function(type, props, children) {
230 + var descriptor = ReactDescriptor.createDescriptor.apply(this, arguments);
231
244 - for (var i = 1; i < arguments.length; i++) {
245 - validateChildKeys(arguments[i], descriptor.type);
246 - }
247 -
248 - var name = descriptor.type.displayName;
249 - if (propTypes) {
250 - checkPropTypes(
251 - name,
252 - propTypes,
253 - descriptor.props,
254 - ReactPropTypeLocations.prop
255 - );
256 - }
257 - if (contextTypes) {
258 - checkPropTypes(
259 - name,
260 - contextTypes,
261 - descriptor._context,
262 - ReactPropTypeLocations.context
263 - );
264 - }
265 - return descriptor;
266 - };
267 -
268 - validatedFactory.prototype = factory.prototype;
269 - validatedFactory.type = factory.type;
232 + for (var i = 2; i < arguments.length; i++) {
233 + validateChildKeys(arguments[i], type);
234 + }
235
271 - // Copy static properties
272 - for (var key in factory) {
273 - if (factory.hasOwnProperty(key)) {
274 - validatedFactory[key] = factory[key];
275 - }
236 + var name = type.displayName;
237 + if (type.propTypes) {
238 + checkPropTypes(
239 + name,
240 + type.propTypes,
241 + descriptor.props,
242 + ReactPropTypeLocations.prop
243 + );
244 }
245 + if (type.contextTypes) {
246 + checkPropTypes(
247 + name,
248 + type.contextTypes,
249 + descriptor._context,
250 + ReactPropTypeLocations.context
251 + );
252 + }
253 + return descriptor;
254 + },
255
256 + createFactory: function(type) {
257 + if (type.isReactLegacyFactory) {
258 + // This is probably a legacy factory created by ReactCompositeComponent.
259 + // We unwrap it to get to the underlying class.
260 + // TODO: Drop this check when we drop ReactLegacyDescriptor.
261 + type = type.type;
262 + }
263 + var validatedFactory = ReactDescriptorValidator.createDescriptor.bind(
264 + null,
265 + type
266 + );
267 + validatedFactory.type = type;
268 return validatedFactory;
269 }
270
src/core/ReactLegacyDescriptor.js new
+63
@@ -0,0 +1,63 @@
1 +/**
2 + * Copyright 2014 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @providesModule ReactLegacyDescriptor
17 + */
18 +
19 +"use strict";
20 +
21 +var ReactDescriptor = require('ReactDescriptor');
22 +
23 +var invariant = require('invariant');
24 +
25 +/**
26 + * Transfer static properties from the source to the target. Functions are
27 + * rebound to have this reflect the original source.
28 + */
29 +function proxyStaticMethods(target, source) {
30 + if (typeof source !== 'function') {
31 + return;
32 + }
33 + for (var key in source) {
34 + if (source.hasOwnProperty(key)) {
35 + var value = source[key];
36 + if (typeof value === 'function') {
37 + target[key] = value.bind(source);
38 + } else {
39 + target[key] = value;
40 + }
41 + }
42 + }
43 +}
44 +
45 +var ReactLegacyDescriptorFactory = {};
46 +
47 +ReactLegacyDescriptorFactory.wrapFactory = function(factory) {
48 + invariant(
49 + ReactDescriptor.isValidFactory(factory),
50 + 'This is suppose to accept a descriptor factory'
51 + );
52 + var legacyDescriptorFactory = function(config, children) {
53 + // This factory should not be called when the new JSX transform is in place.
54 + // TODO: Warning - Use JSX instead of direct function calls.
55 + return factory.apply(this, arguments);
56 + };
57 + proxyStaticMethods(legacyDescriptorFactory, factory.type);
58 + legacyDescriptorFactory.isReactLegacyFactory = true;
59 + legacyDescriptorFactory.type = factory.type;
60 + return legacyDescriptorFactory;
61 +};
62 +
63 +module.exports = ReactLegacyDescriptorFactory;
src/core/ReactPropTransferer.js
-8
@@ -61,14 +61,6 @@ var TransferStrategies = {
61 * Transfer the `className` prop by merging them.
62 */
63 className: createTransferStrategy(joinClasses),
64 - /**
65 - * Never transfer the `key` prop.
66 - */
67 - key: emptyFunction,
68 - /**
69 - * Never transfer the `ref` prop.
70 - */
71 - ref: emptyFunction,
64 /**
65 * Transfer the `style` prop (which is an object) by merging them.
66 */
src/core/__tests__/ReactCompositeComponent-test.js
+37 -59
@@ -417,7 +417,7 @@ describe('ReactCompositeComponent', function() {
417 it('should use default values for undefined props', function() {
418 var Component = React.createClass({
419 getDefaultProps: function() {
420 - return {key: 'testKey'};
420 + return {prop: 'testKey'};
421 },
422 render: function() {
423 return <span />;
@@ -426,21 +426,21 @@ describe('ReactCompositeComponent', function() {
426
427 var instance1 = <Component />;
428 instance1 = ReactTestUtils.renderIntoDocument(instance1);
429 - reactComponentExpect(instance1).scalarPropsEqual({key: 'testKey'});
429 + reactComponentExpect(instance1).scalarPropsEqual({prop: 'testKey'});
430
431 - var instance2 = <Component key={undefined} />;
431 + var instance2 = <Component prop={undefined} />;
432 instance2 = ReactTestUtils.renderIntoDocument(instance2);
433 - reactComponentExpect(instance2).scalarPropsEqual({key: 'testKey'});
433 + reactComponentExpect(instance2).scalarPropsEqual({prop: 'testKey'});
434
435 - var instance3 = <Component key={null} />;
435 + var instance3 = <Component prop={null} />;
436 instance3 = ReactTestUtils.renderIntoDocument(instance3);
437 - reactComponentExpect(instance3).scalarPropsEqual({key: null});
437 + reactComponentExpect(instance3).scalarPropsEqual({prop: null});
438 });
439
440 it('should not mutate passed-in props object', function() {
441 var Component = React.createClass({
442 getDefaultProps: function() {
443 - return {key: 'testKey'};
443 + return {prop: 'testKey'};
444 },
445 render: function() {
446 return <span />;
@@ -450,14 +450,14 @@ describe('ReactCompositeComponent', function() {
450 var inputProps = {};
451 var instance1 = Component(inputProps);
452 instance1 = ReactTestUtils.renderIntoDocument(instance1);
453 - expect(instance1.props.key).toBe('testKey');
453 + expect(instance1.props.prop).toBe('testKey');
454
455 // We don't mutate the input, just in case the caller wants to do something
456 // with it after using it to instantiate a component
457 - expect(inputProps.key).not.toBeDefined();
457 + expect(inputProps.prop).not.toBeDefined();
458 });
459
460 - it('should use default prop value when removing a key', function() {
460 + it('should use default prop value when removing a prop', function() {
461 var Component = React.createClass({
462 getDefaultProps: function() {
463 return {fruit: 'persimmon'};
@@ -480,38 +480,38 @@ describe('ReactCompositeComponent', function() {
480
481 it('should normalize props with default values', function() {
482 var Component = React.createClass({
483 - propTypes: {key: ReactPropTypes.string.isRequired},
483 + propTypes: {prop: ReactPropTypes.string.isRequired},
484 getDefaultProps: function() {
485 - return {key: 'testKey'};
485 + return {prop: 'testKey'};
486 },
487 getInitialState: function() {
488 - return {key: this.props.key + 'State'};
488 + return {prop: this.props.prop + 'State'};
489 },
490 render: function() {
491 - return <span>{this.props.key}</span>;
491 + return <span>{this.props.prop}</span>;
492 }
493 });
494
495 var instance = ReactTestUtils.renderIntoDocument(<Component />);
496 - reactComponentExpect(instance).scalarPropsEqual({key: 'testKey'});
497 - reactComponentExpect(instance).scalarStateEqual({key: 'testKeyState'});
496 + reactComponentExpect(instance).scalarPropsEqual({prop: 'testKey'});
497 + reactComponentExpect(instance).scalarStateEqual({prop: 'testKeyState'});
498
499 - ReactTestUtils.renderIntoDocument(<Component key={null} />);
499 + ReactTestUtils.renderIntoDocument(<Component prop={null} />);
500
501 expect(console.warn.mock.calls.length).toBe(1);
502 expect(console.warn.mock.calls[0][0]).toBe(
503 - 'Warning: Required prop `key` was not specified in `Component`.'
503 + 'Warning: Required prop `prop` was not specified in `Component`.'
504 );
505 });
506
507 it('should check default prop values', function() {
508 var Component = React.createClass({
509 - propTypes: {key: ReactPropTypes.string.isRequired},
509 + propTypes: {prop: ReactPropTypes.string.isRequired},
510 getDefaultProps: function() {
511 - return {key: null};
511 + return {prop: null};
512 },
513 render: function() {
514 - return <span>{this.props.key}</span>;
514 + return <span>{this.props.prop}</span>;
515 }
516 });
517
@@ -519,34 +519,34 @@ describe('ReactCompositeComponent', function() {
519
520 expect(console.warn.mock.calls.length).toBe(1);
521 expect(console.warn.mock.calls[0][0]).toBe(
522 - 'Warning: Required prop `key` was not specified in `Component`.'
522 + 'Warning: Required prop `prop` was not specified in `Component`.'
523 );
524 });
525
526 it('should check declared prop types', function() {
527 var Component = React.createClass({
528 propTypes: {
529 - key: ReactPropTypes.string.isRequired
529 + prop: ReactPropTypes.string.isRequired
530 },
531 render: function() {
532 - return <span>{this.props.key}</span>;
532 + return <span>{this.props.prop}</span>;
533 }
534 });
535
536 ReactTestUtils.renderIntoDocument(<Component />);
537 - ReactTestUtils.renderIntoDocument(<Component key={42} />);
537 + ReactTestUtils.renderIntoDocument(<Component prop={42} />);
538
539 expect(console.warn.mock.calls.length).toBe(2);
540 expect(console.warn.mock.calls[0][0]).toBe(
541 - 'Warning: Required prop `key` was not specified in `Component`.'
541 + 'Warning: Required prop `prop` was not specified in `Component`.'
542 );
543
544 expect(console.warn.mock.calls[1][0]).toBe(
545 - 'Warning: Invalid prop `key` of type `number` supplied to ' +
545 + 'Warning: Invalid prop `prop` of type `number` supplied to ' +
546 '`Component`, expected `string`.'
547 );
548
549 - ReactTestUtils.renderIntoDocument(<Component key="string" />);
549 + ReactTestUtils.renderIntoDocument(<Component prop="string" />);
550
551 // Should not error for strings
552 expect(console.warn.mock.calls.length).toBe(2);
@@ -557,14 +557,14 @@ describe('ReactCompositeComponent', function() {
557 React.createClass({
558 displayName: 'Component',
559 propTypes: {
560 - key: null
560 + prop: null
561 },
562 render: function() {
563 - return <span>{this.props.key}</span>;
563 + return <span>{this.props.prop}</span>;
564 }
565 });
566 }).toThrow(
567 - 'Invariant Violation: Component: prop type `key` is invalid; ' +
567 + 'Invariant Violation: Component: prop type `prop` is invalid; ' +
568 'it must be a function, usually from React.PropTypes.'
569 );
570 });
@@ -574,14 +574,14 @@ describe('ReactCompositeComponent', function() {
574 React.createClass({
575 displayName: 'Component',
576 contextTypes: {
577 - key: null
577 + prop: null
578 },
579 render: function() {
580 - return <span>{this.props.key}</span>;
580 + return <span>{this.props.prop}</span>;
581 }
582 });
583 }).toThrow(
584 - 'Invariant Violation: Component: context type `key` is invalid; ' +
584 + 'Invariant Violation: Component: context type `prop` is invalid; ' +
585 'it must be a function, usually from React.PropTypes.'
586 );
587 });
@@ -591,14 +591,14 @@ describe('ReactCompositeComponent', function() {
591 React.createClass({
592 displayName: 'Component',
593 childContextTypes: {
594 - key: null
594 + prop: null
595 },
596 render: function() {
597 - return <span>{this.props.key}</span>;
597 + return <span>{this.props.prop}</span>;
598 }
599 });
600 }).toThrow(
601 - 'Invariant Violation: Component: child context type `key` is invalid; ' +
601 + 'Invariant Violation: Component: child context type `prop` is invalid; ' +
602 'it must be a function, usually from React.PropTypes.'
603 );
604 });
@@ -1398,28 +1398,6 @@ describe('ReactCompositeComponent', function() {
1398 instance = ReactTestUtils.renderIntoDocument(instance);
1399 });
1400
1401 - it('should allow static methods called using type property', function() {
1402 - spyOn(console, 'warn');
1403 -
1404 - var ComponentClass = React.createClass({
1405 - statics: {
1406 - someStaticMethod: function() {
1407 - return 'someReturnValue';
1408 - }
1409 - },
1410 - getInitialState: function() {
1411 - return {valueToReturn: 'hi'};
1412 - },
1413 - render: function() {
1414 - return <div></div>;
1415 - }
1416 - });
1417 -
1418 - var descriptor = <ComponentClass />;
1419 - expect(descriptor.type.someStaticMethod()).toBe('someReturnValue');
1420 - expect(console.warn.argsForCall.length).toBe(0);
1421 - });
1422 -
1401 it('should disallow nested render calls', function() {
1402 spyOn(console, 'warn');
1403 var Inner = React.createClass({
src/core/__tests__/ReactDescriptor-test.js
+213 -15
@@ -21,14 +21,207 @@
21
22 var React;
23 var ReactDescriptor;
24 +var ReactTestUtils;
25
26 describe('ReactDescriptor', function() {
27 + var ComponentFactory;
28 + var ComponentClass;
29 +
30 beforeEach(function() {
31 React = require('React');
32 ReactDescriptor = require('ReactDescriptor');
33 + ReactTestUtils = require('ReactTestUtils');
34 + ComponentFactory = React.createClass({
35 + render: function() { return <div />; }
36 + });
37 + ComponentClass = ComponentFactory.type;
38 + });
39 +
40 + it('returns a complete descriptor according to spec', function() {
41 + var descriptor = React.createFactory(ComponentClass)();
42 + expect(descriptor.type).toBe(ComponentClass);
43 + expect(descriptor.key).toBe(null);
44 + expect(descriptor.ref).toBe(null);
45 + expect(descriptor.props).toEqual({});
46 + });
47 +
48 + it('allows a string to be passed as the type', function() {
49 + var descriptor = React.createFactory('div')();
50 + expect(descriptor.type).toBe('div');
51 + expect(descriptor.key).toBe(null);
52 + expect(descriptor.ref).toBe(null);
53 + expect(descriptor.props).toEqual({});
54 + });
55 +
56 + it('returns an immutable descriptor', function() {
57 + var descriptor = React.createFactory(ComponentClass)();
58 + expect(() => descriptor.type = 'div').toThrow();
59 + });
60 +
61 + it('does not reuse the original config object', function() {
62 + var config = { foo: 1 };
63 + var descriptor = React.createFactory(ComponentClass)(config);
64 + expect(descriptor.props.foo).toBe(1);
65 + config.foo = 2;
66 + expect(descriptor.props.foo).toBe(1);
67 + });
68 +
69 + it('extracts key and ref from the config', function() {
70 + var descriptor = React.createFactory(ComponentClass)({
71 + key: '12',
72 + ref: '34',
73 + foo: '56'
74 + });
75 + expect(descriptor.type).toBe(ComponentClass);
76 + expect(descriptor.key).toBe('12');
77 + expect(descriptor.ref).toBe('34');
78 + expect(descriptor.props).toEqual({foo:'56'});
79 + });
80 +
81 + it('coerces the key to a string', function() {
82 + var descriptor = React.createFactory(ComponentClass)({
83 + key: 12,
84 + foo: '56'
85 + });
86 + expect(descriptor.type).toBe(ComponentClass);
87 + expect(descriptor.key).toBe('12');
88 + expect(descriptor.ref).toBe(null);
89 + expect(descriptor.props).toEqual({foo:'56'});
90 + });
91 +
92 + it('preserves the context on the descriptor', function() {
93 + var Component = React.createFactory(ComponentClass);
94 + var descriptor;
95 +
96 + var Wrapper = React.createClass({
97 + childContextTypes: {
98 + foo: React.PropTypes.string
99 + },
100 + getChildContext: function() {
101 + return { foo: 'bar' };
102 + },
103 + render: function() {
104 + descriptor = Component();
105 + return descriptor;
106 + }
107 + });
108 +
109 + ReactTestUtils.renderIntoDocument(<Wrapper />);
110 +
111 + expect(descriptor._context).toEqual({ foo: 'bar' });
112 + });
113 +
114 + it('preserves the owner on the descriptor', function() {
115 + var Component = React.createFactory(ComponentClass);
116 + var descriptor;
117 +
118 + var Wrapper = React.createClass({
119 + childContextTypes: {
120 + foo: React.PropTypes.string
121 + },
122 + getChildContext: function() {
123 + return { foo: 'bar' };
124 + },
125 + render: function() {
126 + descriptor = Component();
127 + return descriptor;
128 + }
129 + });
130 +
131 + var instance = ReactTestUtils.renderIntoDocument(<Wrapper />);
132 +
133 + expect(descriptor._owner).toBe(instance);
134 + });
135 +
136 + it('merges an additional argument onto the children prop', function() {
137 + spyOn(console, 'warn');
138 + var a = 1;
139 + var descriptor = React.createFactory(ComponentClass)({
140 + children: 'text'
141 + }, a);
142 + expect(descriptor.props.children).toBe(a);
143 + expect(console.warn.argsForCall.length).toBe(0);
144 });
145
31 - it('should identify valid descriptors correctly', function() {
146 + it('does not override children if no rest args are provided', function() {
147 + spyOn(console, 'warn');
148 + var descriptor = React.createFactory(ComponentClass)({
149 + children: 'text'
150 + });
151 + expect(descriptor.props.children).toBe('text');
152 + expect(console.warn.argsForCall.length).toBe(0);
153 + });
154 +
155 + it('overrides children if null is provided as an argument', function() {
156 + spyOn(console, 'warn');
157 + var descriptor = React.createFactory(ComponentClass)({
158 + children: 'text'
159 + }, null);
160 + expect(descriptor.props.children).toBe(null);
161 + expect(console.warn.argsForCall.length).toBe(0);
162 + });
163 +
164 + it('merges rest arguments onto the children prop in an array', function() {
165 + spyOn(console, 'warn');
166 + var a = 1, b = 2, c = 3;
167 + var descriptor = React.createFactory(ComponentClass)(null, a, b, c);
168 + expect(descriptor.props.children).toEqual([1, 2, 3]);
169 + expect(console.warn.argsForCall.length).toBe(0);
170 + });
171 +
172 + it('warns for keys for arrays of descriptors in rest args', function() {
173 + spyOn(console, 'warn');
174 + var Component = React.createFactory(ComponentClass);
175 +
176 + Component(null, [ Component(), Component() ]);
177 +
178 + expect(console.warn.argsForCall.length).toBe(1);
179 + expect(console.warn.argsForCall[0][0]).toContain(
180 + 'Each child in an array should have a unique "key" prop'
181 + );
182 + });
183 +
184 + it('does not warn when the descriptor is directly in rest args', function() {
185 + spyOn(console, 'warn');
186 + var Component = React.createFactory(ComponentClass);
187 +
188 + Component(null, Component(), Component());
189 +
190 + expect(console.warn.argsForCall.length).toBe(0);
191 + });
192 +
193 + it('does not warn when the array contains a non-descriptor', function() {
194 + spyOn(console, 'warn');
195 + var Component = React.createFactory(ComponentClass);
196 +
197 + Component(null, [ {}, {} ]);
198 +
199 + expect(console.warn.argsForCall.length).toBe(0);
200 + });
201 +
202 + it('allows static methods to be called using the type property', function() {
203 + spyOn(console, 'warn');
204 +
205 + var ComponentClass = React.createClass({
206 + statics: {
207 + someStaticMethod: function() {
208 + return 'someReturnValue';
209 + }
210 + },
211 + getInitialState: function() {
212 + return {valueToReturn: 'hi'};
213 + },
214 + render: function() {
215 + return <div></div>;
216 + }
217 + });
218 +
219 + var descriptor = <ComponentClass />;
220 + expect(descriptor.type.someStaticMethod()).toBe('someReturnValue');
221 + expect(console.warn.argsForCall.length).toBe(0);
222 + });
223 +
224 + it('identifies valid descriptors', function() {
225 var Component = React.createClass({
226 render: function() {
227 return <div />;
@@ -46,21 +239,26 @@ describe('ReactDescriptor', function() {
239 expect(ReactDescriptor.isValidDescriptor(Component)).toEqual(false);
240 });
241
49 - it('immediately calls the type passed to createDescriptor', function() {
50 - var a = 1, b = 2, c = 3, d = 4;
51 -
52 - // TODO: Switch back to using jest.genMockFunction when we run jest in
53 - // github repo.
54 - var test = {
55 - foo: function() { return d; }
56 - };
57 -
58 - spyOn(test, 'foo').andCallThrough();
59 -
60 - var result = React.createDescriptor(test.foo, a, b, c);
242 + it('warns but allow a plain function to be immediately invoked', function() {
243 + spyOn(console, 'warn');
244 + // This is a temporary helper to allow JSX with plain functions.
245 + // This allow you to track down these callers and replace them with regular
246 + // function calls.
247 + var factory = ReactDescriptor.createFactory(function (x) {
248 + return 21 + x;
249 + });
250 + expect(factory(21)).toBe(42);
251 + expect(console.warn.argsForCall.length).toBe(1);
252 + expect(console.warn.argsForCall[0][0]).toContain(
253 + 'This JSX uses a plain function.'
254 + );
255 + });
256
62 - expect(result).toBe(d);
63 - expect(test.foo).toHaveBeenCalledWith(a, b, c);
257 + it('should expose the underlying class from a legacy factory', function() {
258 + var Legacy = React.createClass({ render: function() { } });
259 + var factory = React.createFactory(Legacy);
260 + expect(factory.type).toBe(Legacy.type);
261 + expect(factory().type).toBe(Legacy.type);
262 });
263
264 it('allows the use of PropTypes validators in statics', function() {
src/core/instantiateReactComponent.js
+6 -5
@@ -29,11 +29,12 @@ var invariant = require('invariant');
29 * @return {boolean} Returns true if this is a valid descriptor of a Component.
30 */
31 function isValidComponentDescriptor(descriptor) {
32 - return (
33 - descriptor &&
34 - typeof descriptor.type === 'function' &&
35 - typeof descriptor.type.prototype.mountComponent === 'function' &&
36 - typeof descriptor.type.prototype.receiveComponent === 'function'
32 + return !!descriptor && (
33 + (
34 + typeof descriptor.type === 'function' &&
35 + typeof descriptor.type.prototype.mountComponent === 'function' &&
36 + typeof descriptor.type.prototype.receiveComponent === 'function'
37 + ) || typeof descriptor.type === 'string'
38 );
39 }
40
src/core/shouldUpdateReactComponent.js
+3 -4
@@ -32,10 +32,9 @@
32 */
33 function shouldUpdateReactComponent(prevDescriptor, nextDescriptor) {
34 if (prevDescriptor && nextDescriptor &&
35 - prevDescriptor.type === nextDescriptor.type && (
36 - (prevDescriptor.props && prevDescriptor.props.key) ===
37 - (nextDescriptor.props && nextDescriptor.props.key)
38 - ) && prevDescriptor._owner === nextDescriptor._owner) {
35 + prevDescriptor.type === nextDescriptor.type &&
36 + prevDescriptor.key === nextDescriptor.key &&
37 + prevDescriptor._owner === nextDescriptor._owner) {
38 return true;
39 }
40 return false;
src/utils/__tests__/cloneWithProps-test.js
+5 -8
@@ -121,14 +121,11 @@ describe('cloneWithProps', function() {
121 it('should transfer the key property', function() {
122 var Component = React.createClass({
123 render: function() {
124 - expect(this.props.key).toBe('xyz');
125 - return <div />;
124 + return null;
125 }
126 });
128 -
129 - ReactTestUtils.renderIntoDocument(
130 - cloneWithProps(<Component />, {key: 'xyz'})
131 - );
127 + var clone = cloneWithProps(<Component />, {key: 'xyz'});
128 + expect(clone.key).toBe('xyz');
129 });
130
131 it('should transfer children', function() {
@@ -160,8 +157,6 @@ describe('cloneWithProps', function() {
157 it('should support keys and refs', function() {
158 var Component = React.createClass({
159 render: function() {
163 - expect(this.props.key).toBe('xyz');
164 - expect(this.props.ref).toBe('xyz');
160 return <div />;
161 }
162 });
@@ -170,6 +165,8 @@ describe('cloneWithProps', function() {
165 render: function() {
166 var clone =
167 cloneWithProps(this.props.children, {key: 'xyz', ref: 'xyz'});
168 + expect(clone.key).toBe('xyz');
169 + expect(clone.ref).toBe('xyz');
170 return <div>{clone}</div>;
171 }
172 });
src/utils/cloneWithProps.js
+3 -2
@@ -19,6 +19,7 @@
19
20 "use strict";
21
22 +var ReactDescriptor = require('ReactDescriptor');
23 var ReactPropTransferer = require('ReactPropTransferer');
24
25 var keyOf = require('keyOf');
@@ -38,7 +39,7 @@ var CHILDREN_PROP = keyOf({children: null});
39 function cloneWithProps(child, props) {
40 if (__DEV__) {
41 warning(
41 - !child.props.ref,
42 + !child.ref,
43 'You are calling cloneWithProps() on a child with a ref. This is ' +
44 'dangerous because you\'re creating a new child which will not be ' +
45 'added as a ref to its parent.'
@@ -55,7 +56,7 @@ function cloneWithProps(child, props) {
56
57 // The current API doesn't retain _owner and _context, which is why this
58 // doesn't use ReactDescriptor.cloneAndReplaceProps.
58 - return child.constructor(newProps);
59 + return ReactDescriptor.createDescriptor(child.type, newProps);
60 }
61
62 module.exports = cloneWithProps;
src/utils/traverseAllChildren.js
+2 -2
@@ -54,9 +54,9 @@ function userProvidedKeyEscaper(match) {
54 * @return {string}
55 */
56 function getComponentKey(component, index) {
57 - if (component && component.props && component.props.key != null) {
57 + if (component && component.key != null) {
58 // Explicit key
59 - return wrapUserProvidedKey(component.props.key);
59 + return wrapUserProvidedKey(component.key);
60 }
61 // Implicit key determined by the index in the set
62 return index.toString(36);