diff --git a/src/browser/ReactDOM.js b/src/browser/ReactDOM.js index abe0630302..b7eeecf004 100644 --- a/src/browser/ReactDOM.js +++ b/src/browser/ReactDOM.js @@ -51,6 +51,12 @@ function createDOMComponentClass(tag, omitClose) { return instance; }; + // Expose the constructor on the ConvenienceConstructor and prototype so that + // it can be easily easily accessed on descriptors. + // E.g.
.type === div.type + ConvenienceConstructor.type = Constructor; + Constructor.prototype.type = Constructor; + Constructor.ConvenienceConstructor = ConvenienceConstructor; ConvenienceConstructor.componentConstructor = Constructor; return ConvenienceConstructor; diff --git a/src/browser/ReactTextComponent.js b/src/browser/ReactTextComponent.js index 79114b9e7b..63a69ec1c1 100644 --- a/src/browser/ReactTextComponent.js +++ b/src/browser/ReactTextComponent.js @@ -91,4 +91,9 @@ mixInto(ReactTextComponent, { }); +// Expose the constructor on itself and the prototype for consistency with other +// descriptors. +ReactTextComponent.type = ReactTextComponent; +ReactTextComponent.prototype.type = ReactTextComponent; + module.exports = ReactTextComponent; diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index 64f9d7652d..49de894323 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -183,10 +183,17 @@ var ReactComponent = { * @final */ isValidComponent: function(object) { - return !!( - object && - typeof object.mountComponentIntoNode === 'function' && - typeof object.receiveComponent === 'function' + if (!object || !object.type || !object.type.prototype) { + return false; + } + // This is the safer way of duck checking the type of instance this is. + // The object can be a generic descriptor but the type property refers to + // the constructor and it's prototype can be used to inspect the type that + // will actually get mounted. + var prototype = object.type.prototype; + return ( + typeof prototype.mountComponentIntoNode === 'function' && + typeof prototype.receiveComponent === 'function' ); }, diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index 63b1e8358d..b8598e9b00 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -584,6 +584,118 @@ function createChainedFunction(one, two) { }; } +if (__DEV__) { + + var unmountedPropertyWhitelist = { + constructor: true, + construct: true, + isOwnedBy: true, // should be deprecated but can have code mod (internal) + mountComponent: true, + mountComponentIntoNode: true, + props: true, + type: true, + _checkPropTypes: true, + _mountComponentIntoNode: true, + _processContext: true + }; + + var hasWarnedOnComponentType = {}; + + var warnIfUnmounted = function(instance, key) { + if (instance.__hasBeenMounted) { + return; + } + var name = instance.constructor.displayName || 'Unknown'; + var owner = ReactCurrentOwner.current; + var ownerName = (owner && owner.constructor.displayName) || 'Unknown'; + var warningKey = key + '|' + name + '|' + ownerName; + if (hasWarnedOnComponentType.hasOwnProperty(warningKey)) { + // We have already warned for this combination. Skip it this time. + return; + } + hasWarnedOnComponentType[warningKey] = true; + + var context = owner ? ' in ' + ownerName + '.' : ' at the top level.'; + var staticMethodExample = '<' + name + ' />.type.' + key + '(...)'; + + console.warn( + 'Invalid access to component property "' + key + '" on ' + name + + context + ' See http://fb.me/react-warning-descriptors .' + + ' Use a static method instead: ' + staticMethodExample + ); + }; + + var defineMembraneProperty = function(membrane, prototype, key) { + Object.defineProperty(membrane, key, { + + configurable: false, + enumerable: true, + + get: function() { + if (this !== membrane) { + // When this is accessed through a prototype chain we need to check if + // this component was mounted. + warnIfUnmounted(this, key); + } + return prototype[key]; + }, + + set: function(value) { + if (this !== membrane) { + // When this is accessed through a prototype chain, we first check if + // this component was mounted. Then we define a value on "this" + // instance, effectively disabling the membrane on that prototype + // chain. + warnIfUnmounted(this, key); + Object.defineProperty(this, key, { + enumerable: true, + configurable: true, + writable: true, + value: value + }); + } else { + // Otherwise, this should modify the prototype + prototype[key] = value; + } + } + + }); + }; + + /** + * Creates a membrane prototype which wraps the original prototype. If any + * property is accessed in an unmounted state, a warning is issued. + * + * @param {object} prototype Original prototype. + * @return {object} The membrane prototype. + * @private + */ + var createMountWarningMembrane = function(prototype) { + try { + var membrane = Object.create(prototype); + for (var key in prototype) { + if (unmountedPropertyWhitelist.hasOwnProperty(key)) { + continue; + } + defineMembraneProperty(membrane, prototype, key); + } + + membrane.mountComponent = function() { + this.__hasBeenMounted = true; + return prototype.mountComponent.apply(this, arguments); + }; + + return membrane; + } catch(x) { + // In IE8 define property will fail on non-DOM objects. If anything in + // the membrane creation fails, we'll bail out and just use the prototype + // without warnings. + return prototype; + } + }; + +} + /** * `ReactCompositeComponent` maintains an auxiliary life cycle state in * `this._compositeLifeCycleState` (which can be null). @@ -1307,6 +1419,14 @@ var ReactCompositeComponent = { } } + // Expose the convience constructor on the prototype so that it can be + // easily accessed on descriptors. E.g.