diff --git a/src/browser/ui/ReactBrowserComponentMixin.js b/src/browser/ui/ReactBrowserComponentMixin.js
index f48669b4cc80f..2322b73a22da5 100644
--- a/src/browser/ui/ReactBrowserComponentMixin.js
+++ b/src/browser/ui/ReactBrowserComponentMixin.js
@@ -11,7 +11,6 @@
"use strict";
-var ReactEmptyComponent = require('ReactEmptyComponent');
var ReactMount = require('ReactMount');
var invariant = require('invariant');
@@ -29,10 +28,7 @@ var ReactBrowserComponentMixin = {
this.isMounted(),
'getDOMNode(): A component must be mounted to have a DOM node.'
);
- if (ReactEmptyComponent.isNullComponentID(this._rootNodeID)) {
- return null;
- }
- return ReactMount.getNode(this._rootNodeID);
+ return ReactMount.getNodeFromInstance(this);
}
};
diff --git a/src/browser/ui/ReactMount.js b/src/browser/ui/ReactMount.js
index 642fe67e5e47e..b62e56080a969 100644
--- a/src/browser/ui/ReactMount.js
+++ b/src/browser/ui/ReactMount.js
@@ -15,7 +15,9 @@ var DOMProperty = require('DOMProperty');
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactElement = require('ReactElement');
+var ReactEmptyComponent = require('ReactEmptyComponent');
var ReactInstanceHandles = require('ReactInstanceHandles');
+var ReactInstanceMap = require('ReactInstanceMap');
var ReactPerf = require('ReactPerf');
var containsNode = require('containsNode');
@@ -125,6 +127,30 @@ function getNode(id) {
return nodeCache[id];
}
+/**
+ * Finds the node with the supplied public React instance.
+ *
+ * @param {*} instance A public React instance.
+ * @return {?DOMElement} DOM node with the suppled `id`.
+ * @internal
+ */
+function getNodeFromInstance(instance) {
+ // This instance can currently be either a public or private instance since
+ // native nodes are still public.
+ var id = instance._rootNodeID;
+ // TODO: Once these are only public instances, remove this conditional.
+ if (id == null) {
+ id = ReactInstanceMap.get(instance)._rootNodeID;
+ }
+ if (ReactEmptyComponent.isNullComponentID(id)) {
+ return null;
+ }
+ if (!nodeCache.hasOwnProperty(id) || !isValid(nodeCache[id], id)) {
+ nodeCache[id] = ReactMount.findReactNodeByID(id);
+ }
+ return nodeCache[id];
+}
+
/**
* A node is "valid" if it is contained by a currently mounted container.
*
@@ -358,7 +384,7 @@ var ReactMount = {
nextElement,
container,
callback
- );
+ ).getPublicInstance();
} else {
ReactMount.unmountComponentAtNode(container);
}
@@ -374,7 +400,7 @@ var ReactMount = {
nextElement,
container,
shouldReuseMarkup
- );
+ ).getPublicInstance();
callback && callback.call(component);
return component;
},
@@ -677,6 +703,8 @@ var ReactMount = {
getNode: getNode,
+ getNodeFromInstance: getNodeFromInstance,
+
purgeID: purgeID
};
diff --git a/src/browser/ui/__tests__/ReactDOMComponent-test.js b/src/browser/ui/__tests__/ReactDOMComponent-test.js
index 2d54865dc3b78..1257e11640477 100644
--- a/src/browser/ui/__tests__/ReactDOMComponent-test.js
+++ b/src/browser/ui/__tests__/ReactDOMComponent-test.js
@@ -21,41 +21,39 @@ describe('ReactDOMComponent', function() {
describe('updateDOM', function() {
var React;
var ReactTestUtils;
- var transaction;
beforeEach(function() {
React = require('React');
ReactTestUtils = require('ReactTestUtils');
-
- var ReactReconcileTransaction = require('ReactReconcileTransaction');
- transaction = new ReactReconcileTransaction();
});
it("should handle className", function() {
- var stub = ReactTestUtils.renderIntoDocument(
);
-
- stub.receiveComponent({props: { className: 'foo' }}, transaction);
- expect(stub.getDOMNode().className).toEqual('foo');
- stub.receiveComponent({props: { className: 'bar' }}, transaction);
- expect(stub.getDOMNode().className).toEqual('bar');
- stub.receiveComponent({props: { className: null }}, transaction);
- expect(stub.getDOMNode().className).toEqual('');
+ var container = document.createElement('div');
+ React.render(, container);
+
+ React.render(, container);
+ expect(container.firstChild.className).toEqual('foo');
+ React.render(, container);
+ expect(container.firstChild.className).toEqual('bar');
+ React.render(, container);
+ expect(container.firstChild.className).toEqual('');
});
it("should gracefully handle various style value types", function() {
- var stub = ReactTestUtils.renderIntoDocument();
- var stubStyle = stub.getDOMNode().style;
+ var container = document.createElement('div');
+ React.render(, container);
+ var stubStyle = container.firstChild.style;
// set initial style
var setup = { display: 'block', left: '1', top: 2, fontFamily: 'Arial' };
- stub.receiveComponent({props: { style: setup }}, transaction);
+ React.render(, container);
expect(stubStyle.display).toEqual('block');
expect(stubStyle.left).toEqual('1px');
expect(stubStyle.fontFamily).toEqual('Arial');
// reset the style to their default state
var reset = { display: '', left: null, top: false, fontFamily: true };
- stub.receiveComponent({props: { style: reset }}, transaction);
+ React.render(, container);
expect(stubStyle.display).toEqual('');
expect(stubStyle.left).toEqual('');
expect(stubStyle.top).toEqual('');
@@ -64,34 +62,35 @@ describe('ReactDOMComponent', function() {
it("should update styles when mutating style object", function() {
var styles = { display: 'none', fontFamily: 'Arial', lineHeight: 1.2 };
- var stub = ReactTestUtils.renderIntoDocument();
+ var container = document.createElement('div');
+ React.render(, container);
- var stubStyle = stub.getDOMNode().style;
+ var stubStyle = container.firstChild.style;
stubStyle.display = styles.display;
stubStyle.fontFamily = styles.fontFamily;
styles.display = 'block';
- stub.receiveComponent({props: { style: styles }}, transaction);
+ React.render(, container);
expect(stubStyle.display).toEqual('block');
expect(stubStyle.fontFamily).toEqual('Arial');
expect(stubStyle.lineHeight).toEqual('1.2');
styles.fontFamily = 'Helvetica';
- stub.receiveComponent({props: { style: styles }}, transaction);
+ React.render(, container);
expect(stubStyle.display).toEqual('block');
expect(stubStyle.fontFamily).toEqual('Helvetica');
expect(stubStyle.lineHeight).toEqual('1.2');
styles.lineHeight = 0.5;
- stub.receiveComponent({props: { style: styles }}, transaction);
+ React.render(, container);
expect(stubStyle.display).toEqual('block');
expect(stubStyle.fontFamily).toEqual('Helvetica');
expect(stubStyle.lineHeight).toEqual('0.5');
- stub.receiveComponent({props: { style: undefined }}, transaction);
+ React.render(, container);
expect(stubStyle.display).toBe('');
expect(stubStyle.fontFamily).toBe('');
expect(stubStyle.lineHeight).toBe('');
@@ -99,92 +98,96 @@ describe('ReactDOMComponent', function() {
it("should update styles if initially null", function() {
var styles = null;
- var stub = ReactTestUtils.renderIntoDocument();
+ var container = document.createElement('div');
+ React.render(, container);
- var stubStyle = stub.getDOMNode().style;
+ var stubStyle = container.firstChild.style;
styles = {display: 'block'};
- stub.receiveComponent({props: { style: styles }}, transaction);
+ React.render(, container);
expect(stubStyle.display).toEqual('block');
});
it("should remove attributes", function() {
- var stub = ReactTestUtils.renderIntoDocument(
);
+ var container = document.createElement('div');
+ React.render(
, container);
- expect(stub.getDOMNode().hasAttribute('height')).toBe(true);
- stub.receiveComponent({props: {}}, transaction);
- expect(stub.getDOMNode().hasAttribute('height')).toBe(false);
+ expect(container.firstChild.hasAttribute('height')).toBe(true);
+ React.render(
, container);
+ expect(container.firstChild.hasAttribute('height')).toBe(false);
});
it("should remove properties", function() {
- var stub = ReactTestUtils.renderIntoDocument();
+ var container = document.createElement('div');
+ React.render(, container);
- expect(stub.getDOMNode().className).toEqual('monkey');
- stub.receiveComponent({props: {}}, transaction);
- expect(stub.getDOMNode().className).toEqual('');
+ expect(container.firstChild.className).toEqual('monkey');
+ React.render(, container);
+ expect(container.firstChild.className).toEqual('');
});
it("should clear a single style prop when changing 'style'", function() {
var styles = {display: 'none', color: 'red'};
- var stub = ReactTestUtils.renderIntoDocument();
+ var container = document.createElement('div');
+ React.render(, container);
- var stubStyle = stub.getDOMNode().style;
+ var stubStyle = container.firstChild.style;
styles = {color: 'green'};
- stub.receiveComponent({props: { style: styles }}, transaction);
+ React.render(, container);
expect(stubStyle.display).toEqual('');
expect(stubStyle.color).toEqual('green');
});
it("should clear all the styles when removing 'style'", function() {
var styles = {display: 'none', color: 'red'};
- var stub = ReactTestUtils.renderIntoDocument();
+ var container = document.createElement('div');
+ React.render(, container);
- var stubStyle = stub.getDOMNode().style;
+ var stubStyle = container.firstChild.style;
- stub.receiveComponent({props: {}}, transaction);
+ React.render(, container);
expect(stubStyle.display).toEqual('');
expect(stubStyle.color).toEqual('');
});
it("should empty element when removing innerHTML", function() {
- var stub = ReactTestUtils.renderIntoDocument(
-
- );
+ var container = document.createElement('div');
+ React.render(, container);
- expect(stub.getDOMNode().innerHTML).toEqual(':)');
- stub.receiveComponent({props: {}}, transaction);
- expect(stub.getDOMNode().innerHTML).toEqual('');
+ expect(container.firstChild.innerHTML).toEqual(':)');
+ React.render(, container);
+ expect(container.firstChild.innerHTML).toEqual('');
});
it("should transition from string content to innerHTML", function() {
- var stub = ReactTestUtils.renderIntoDocument(
- hello
- );
+ var container = document.createElement('div');
+ React.render(hello
, container);
- expect(stub.getDOMNode().innerHTML).toEqual('hello');
- stub.receiveComponent(
- {props: {dangerouslySetInnerHTML: {__html: 'goodbye'}}},
- transaction
+ expect(container.firstChild.innerHTML).toEqual('hello');
+ React.render(
+ ,
+ container
);
- expect(stub.getDOMNode().innerHTML).toEqual('goodbye');
+ expect(container.firstChild.innerHTML).toEqual('goodbye');
});
it("should transition from innerHTML to string content", function() {
- var stub = ReactTestUtils.renderIntoDocument(
-
- );
+ var container = document.createElement('div');
+ React.render(
+ , container);
- expect(stub.getDOMNode().innerHTML).toEqual('bonjour');
- stub.receiveComponent({props: {children: 'adieu'}}, transaction);
- expect(stub.getDOMNode().innerHTML).toEqual('adieu');
+ expect(container.firstChild.innerHTML).toEqual('bonjour');
+ React.render(adieu
, container);
+ expect(container.firstChild.innerHTML).toEqual('adieu');
});
it("should not incur unnecessary DOM mutations", function() {
- var stub = ReactTestUtils.renderIntoDocument();
+ var container = document.createElement('div');
+ React.render(, container);
- var node = stub.getDOMNode();
+ var node = container.firstChild;
var nodeValue = ''; // node.value always returns undefined
var nodeValueSetter = mocks.getMockFunction();
Object.defineProperty(node, 'value', {
@@ -196,10 +199,10 @@ describe('ReactDOMComponent', function() {
})
});
- stub.receiveComponent({props: {value: ''}}, transaction);
+ React.render(, container);
expect(nodeValueSetter.mock.calls.length).toBe(0);
- stub.receiveComponent({props: {}}, transaction);
+ React.render(, container);
expect(nodeValueSetter.mock.calls.length).toBe(1);
});
});
diff --git a/src/browser/ui/__tests__/ReactRenderDocument-test.js b/src/browser/ui/__tests__/ReactRenderDocument-test.js
index 7ebeb2a7c0b1f..b0c644c3a9bb0 100644
--- a/src/browser/ui/__tests__/ReactRenderDocument-test.js
+++ b/src/browser/ui/__tests__/ReactRenderDocument-test.js
@@ -14,6 +14,7 @@
"use strict";
var React;
+var ReactInstanceMap;
var ReactMount;
var getTestDocument;
@@ -32,6 +33,7 @@ describe('rendering React components at document', function() {
require('mock-modules').dumpCache();
React = require('React');
+ ReactInstanceMap = require('ReactInstanceMap');
ReactMount = require('ReactMount');
getTestDocument = require('getTestDocument');
@@ -61,8 +63,10 @@ describe('rendering React components at document', function() {
var component = React.render(, testDocument);
expect(testDocument.body.innerHTML).toBe('Hello world');
+ // TODO: This is a bad test. I have no idea what this is testing.
+ // Node IDs is an implementation detail and not part of the public API.
var componentID = ReactMount.getReactRootID(testDocument);
- expect(componentID).toBe(component._rootNodeID);
+ expect(componentID).toBe(ReactInstanceMap.get(component)._rootNodeID);
});
it('should not be able to unmount component from document node', function() {
diff --git a/src/class/ReactClass.js b/src/class/ReactClass.js
index 13ed707fb5e19..74e424eb4f469 100644
--- a/src/class/ReactClass.js
+++ b/src/class/ReactClass.js
@@ -11,8 +11,10 @@
"use strict";
-var ReactCompositeComponent = require('ReactCompositeComponent');
var ReactElement = require('ReactElement');
+var ReactErrorUtils = require('ReactErrorUtils');
+var ReactInstanceMap = require('ReactInstanceMap');
+var ReactPropTransferer = require('ReactPropTransferer');
var ReactPropTypeLocations = require('ReactPropTypeLocations');
var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
@@ -22,6 +24,7 @@ var keyMirror = require('keyMirror');
var keyOf = require('keyOf');
var monitorCodeUse = require('monitorCodeUse');
var mapObject = require('mapObject');
+var warning = require('warning');
var MIXINS_KEY = keyOf({mixins: null});
@@ -398,7 +401,7 @@ function validateMethodOverride(proto, name) {
null;
// Disallow overriding of base class methods unless explicitly allowed.
- if (ReactCompositeComponent.Base.prototype.hasOwnProperty(name)) {
+ if (ReactClassMixin.hasOwnProperty(name)) {
invariant(
specPolicy === SpecPolicy.OVERRIDE_BASE,
'ReactClassInterface: You are attempting to override ' +
@@ -621,6 +624,218 @@ function createChainedFunction(one, two) {
};
}
+/**
+ * Binds a method to the component.
+ *
+ * @param {object} component Component whose method is going to be bound.
+ * @param {function} method Method to be bound.
+ * @return {function} The bound method.
+ */
+function bindAutoBindMethod(component, method) {
+ var boundMethod = method.bind(component);
+ if (__DEV__) {
+ boundMethod.__reactBoundContext = component;
+ boundMethod.__reactBoundMethod = method;
+ boundMethod.__reactBoundArguments = null;
+ var componentName = component.constructor.displayName;
+ var _bind = boundMethod.bind;
+ boundMethod.bind = function(newThis, ...args) {
+ // User is trying to bind() an autobound method; we effectively will
+ // ignore the value of "this" that the user is trying to use, so
+ // let's warn.
+ if (newThis !== component && newThis !== null) {
+ monitorCodeUse('react_bind_warning', { component: componentName });
+ console.warn(
+ 'bind(): React component methods may only be bound to the ' +
+ 'component instance. See ' + componentName
+ );
+ } else if (!args.length) {
+ monitorCodeUse('react_bind_warning', { component: componentName });
+ console.warn(
+ 'bind(): You are binding a component method to the component. ' +
+ 'React does this for you automatically in a high-performance ' +
+ 'way, so you can safely remove this call. See ' + componentName
+ );
+ return boundMethod;
+ }
+ var reboundMethod = _bind.apply(boundMethod, arguments);
+ reboundMethod.__reactBoundContext = component;
+ reboundMethod.__reactBoundMethod = method;
+ reboundMethod.__reactBoundArguments = args;
+ return reboundMethod;
+ };
+ }
+ return boundMethod;
+}
+
+/**
+ * Binds all auto-bound methods in a component.
+ *
+ * @param {object} component Component whose method is going to be bound.
+ */
+function bindAutoBindMethods(component) {
+ for (var autoBindKey in component.__reactAutoBindMap) {
+ if (component.__reactAutoBindMap.hasOwnProperty(autoBindKey)) {
+ var method = component.__reactAutoBindMap[autoBindKey];
+ component[autoBindKey] = bindAutoBindMethod(
+ component,
+ ReactErrorUtils.guard(
+ method,
+ component.constructor.displayName + '.' + autoBindKey
+ )
+ );
+ }
+ }
+}
+
+/**
+ * @lends {ReactClass.prototype}
+ */
+var ReactClassMixin = {
+
+ /**
+ * Sets a subset of the state. Always use this or `replaceState` to mutate
+ * state. You should treat `this.state` as immutable.
+ *
+ * There is no guarantee that `this.state` will be immediately updated, so
+ * accessing `this.state` after calling this method may return the old value.
+ *
+ * There is no guarantee that calls to `setState` will run synchronously,
+ * as they may eventually be batched together. You can provide an optional
+ * callback that will be executed when the call to setState is actually
+ * completed.
+ *
+ * @param {object} partialState Next partial state to be merged with state.
+ * @param {?function} callback Called after state is updated.
+ * @final
+ * @protected
+ */
+ setState: function(partialState, callback) {
+ invariant(
+ typeof partialState === 'object' || partialState == null,
+ 'setState(...): takes an object of state variables to update.'
+ );
+ if (__DEV__) {
+ warning(
+ partialState != null,
+ 'setState(...): You passed an undefined or null state object; ' +
+ 'instead, use forceUpdate().'
+ );
+ }
+ var internalInstance = ReactInstanceMap.get(this);
+ invariant(
+ internalInstance,
+ 'setState(...): Can only update a mounted or mounting component.'
+ );
+ internalInstance.setState(
+ partialState, callback && callback.bind(this)
+ );
+ },
+
+ /**
+ * TODO: This will be deprecated because state should always keep a consistent
+ * type signature and the only use case for this, is to avoid that.
+ */
+ replaceState: function(newState, callback) {
+ var internalInstance = ReactInstanceMap.get(this);
+ invariant(
+ internalInstance,
+ 'replaceState(...): Can only update a mounted or mounting component.'
+ );
+ internalInstance.replaceState(
+ newState,
+ callback && callback.bind(this)
+ );
+ },
+
+ /**
+ * Forces an update. This should only be invoked when it is known with
+ * certainty that we are **not** in a DOM transaction.
+ *
+ * You may want to call this when you know that some deeper aspect of the
+ * component's state has changed but `setState` was not called.
+ *
+ * This will not invoke `shouldUpdateComponent`, but it will invoke
+ * `componentWillUpdate` and `componentDidUpdate`.
+ *
+ * @param {?function} callback Called after update is complete.
+ * @final
+ * @protected
+ */
+ forceUpdate: function(callback) {
+ var internalInstance = ReactInstanceMap.get(this);
+ invariant(
+ internalInstance,
+ 'forceUpdate(...): Can only force an update on mounted or mounting ' +
+ 'components.'
+ );
+ internalInstance.forceUpdate(callback && callback.bind(this));
+ },
+
+ /**
+ * Checks whether or not this composite component is mounted.
+ * @return {boolean} True if mounted, false otherwise.
+ * @protected
+ * @final
+ */
+ isMounted: function() {
+ var internalInstance = ReactInstanceMap.get(this);
+ // In theory, isMounted is always true if it exists in the map.
+ // TODO: Remove the internal isMounted method.
+ return internalInstance && internalInstance.isMounted();
+ },
+
+ /**
+ * Sets a subset of the props.
+ *
+ * @param {object} partialProps Subset of the next props.
+ * @param {?function} callback Called after props are updated.
+ * @final
+ * @public
+ * @deprecated
+ */
+ setProps: function(partialProps, callback) {
+ var internalInstance = ReactInstanceMap.get(this);
+ invariant(
+ internalInstance,
+ 'setProps(...): Can only update a mounted component.'
+ );
+ internalInstance.setProps(
+ partialProps,
+ callback && callback.bind(this)
+ );
+ },
+
+ /**
+ * Replace all the props.
+ *
+ * @param {object} newProps Subset of the next props.
+ * @param {?function} callback Called after props are updated.
+ * @final
+ * @public
+ * @deprecated
+ */
+ replaceProps: function(newProps, callback) {
+ ReactInstanceMap.get(this).replaceProps(
+ newProps,
+ callback && callback.bind(this)
+ );
+ }
+};
+
+var ReactClassBase = function() {};
+assign(
+ ReactClassBase.prototype,
+ ReactPropTransferer.Mixin,
+ ReactClassMixin
+);
+
+/**
+ * Module for creating composite components.
+ *
+ * @class ReactClass
+ * @extends ReactPropTransferer
+ */
var ReactClass = {
/**
@@ -633,10 +848,14 @@ var ReactClass = {
createClass: function(spec) {
var Constructor = function(props) {
// This constructor is overridden by mocks. The argument is used
- // by mocks to assert on what gets mounted. This will later be used
- // by the stand-alone class implementation.
+ // by mocks to assert on what gets mounted.
+
+ // Wire up auto-binding
+ if (this.__reactAutoBindMap) {
+ bindAutoBindMethods(this);
+ }
};
- Constructor.prototype = new ReactCompositeComponent.Base();
+ Constructor.prototype = new ReactClassBase();
Constructor.prototype.constructor = Constructor;
injectedMixins.forEach(
diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js
index 7a7b9688d1bd7..2e400d3e201ec 100644
--- a/src/core/ReactComponent.js
+++ b/src/core/ReactComponent.js
@@ -442,6 +442,18 @@ var ReactComponent = {
return null;
}
return owner.refs[ref];
+ },
+
+ /**
+ * Get the publicly accessible representation of this component - i.e. what
+ * is exposed by refs and renderComponent. Can be null for stateless
+ * components.
+ *
+ * @return {?ReactComponent} the actual sibling Component.
+ * @internal
+ */
+ getPublicInstance: function() {
+ return this;
}
}
};
diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js
index 0c1f187e144e2..b78909c6bd126 100644
--- a/src/core/ReactCompositeComponent.js
+++ b/src/core/ReactCompositeComponent.js
@@ -16,37 +16,31 @@ var ReactContext = require('ReactContext');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactElement = require('ReactElement');
var ReactEmptyComponent = require('ReactEmptyComponent');
-var ReactErrorUtils = require('ReactErrorUtils');
+var ReactInstanceMap = require('ReactInstanceMap');
var ReactOwner = require('ReactOwner');
var ReactPerf = require('ReactPerf');
-var ReactPropTransferer = require('ReactPropTransferer');
var ReactPropTypeLocations = require('ReactPropTypeLocations');
var ReactUpdates = require('ReactUpdates');
var assign = require('Object.assign');
-var instantiateReactComponent = require('instantiateReactComponent');
var invariant = require('invariant');
var keyMirror = require('keyMirror');
-var monitorCodeUse = require('monitorCodeUse');
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
var warning = require('warning');
function getDeclarationErrorAddendum(component) {
var owner = component._owner || null;
- if (owner && owner.constructor && owner.constructor.displayName) {
- return ' Check the render method of `' + owner.constructor.displayName +
- '`.';
+ if (owner) {
+ var constructor = owner._instance.constructor;
+ if (constructor && constructor.displayName) {
+ return ' Check the render method of `' + constructor.displayName + '`.';
+ }
}
return '';
}
function validateLifeCycleOnReplaceState(instance) {
var compositeLifeCycleState = instance._compositeLifeCycleState;
- invariant(
- instance.isMounted() ||
- compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
- 'replaceState(...): Can only update a mounted or mounting component.'
- );
invariant(
ReactCurrentOwner.current == null,
'replaceState(...): Cannot update during an existing state transition ' +
@@ -106,7 +100,9 @@ var CompositeLifeCycle = keyMirror({
/**
* @lends {ReactCompositeComponent.prototype}
*/
-var ReactCompositeComponentMixin = {
+var ReactCompositeComponentMixin = assign({},
+ ReactComponent.Mixin,
+ ReactOwner.Mixin, {
/**
* Base constructor for all composite component.
@@ -116,18 +112,16 @@ var ReactCompositeComponentMixin = {
* @internal
*/
construct: function(element) {
- // Children can be either an array or more than one argument
- ReactComponent.Mixin.construct.apply(this, arguments);
- ReactOwner.Mixin.construct.apply(this, arguments);
+ this._instance.props = element.props;
+ this._instance.state = null;
+ this._instance.context = null;
- this.state = null;
this._pendingState = null;
-
- // This is the public post-processed context. The real context and pending
- // context lives on the element.
- this.context = null;
-
this._compositeLifeCycleState = null;
+
+ // Children can be either an array or more than one argument
+ ReactComponent.Mixin.construct.apply(this, arguments);
+ ReactOwner.Mixin.construct.apply(this, arguments);
},
/**
@@ -161,47 +155,48 @@ var ReactCompositeComponentMixin = {
transaction,
mountDepth
);
- this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;
- if (this.__reactAutoBindMap) {
- this._bindAutoBindMethods();
- }
+ var inst = this._instance;
+
+ // Store a reference from the instance back to the internal representation
+ ReactInstanceMap.set(inst, this);
- this.context = this._processContext(this._currentElement._context);
- this.props = this._processProps(this.props);
+ this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;
- this.state = this.getInitialState ? this.getInitialState() : null;
+ inst.context = this._processContext(this._currentElement._context);
+ inst.props = this._processProps(this._currentElement.props);
+ var initialState = inst.getInitialState ? inst.getInitialState() : null;
if (__DEV__) {
// We allow auto-mocks to proceed as if they're returning null.
- if (typeof this.state === 'undefined' &&
- this.getInitialState && this.getInitialState._isMockFunction) {
+ if (typeof initialState === 'undefined' &&
+ inst.getInitialState._isMockFunction) {
// This is probably bad practice. Consider warning here and
// deprecating this convenience.
- this.state = null;
+ initialState = null;
}
}
-
invariant(
- typeof this.state === 'object' && !Array.isArray(this.state),
+ typeof initialState === 'object' && !Array.isArray(initialState),
'%s.getInitialState(): must return an object or null',
- this.constructor.displayName || 'ReactCompositeComponent'
+ inst.constructor.displayName || 'ReactCompositeComponent'
);
+ inst.state = initialState;
this._pendingState = null;
this._pendingForceUpdate = false;
- if (this.componentWillMount) {
- this.componentWillMount();
+ if (inst.componentWillMount) {
+ inst.componentWillMount();
// When mounting, calls to `setState` by `componentWillMount` will set
// `this._pendingState` without triggering a re-render.
if (this._pendingState) {
- this.state = this._pendingState;
+ inst.state = this._pendingState;
this._pendingState = null;
}
}
- this._renderedComponent = instantiateReactComponent(
+ this._renderedComponent = this._instantiateReactComponent(
this._renderValidatedComponent(),
this._currentElement.type // The wrapping type
);
@@ -213,8 +208,8 @@ var ReactCompositeComponentMixin = {
transaction,
mountDepth + 1
);
- if (this.componentDidMount) {
- transaction.getReactMountReady().enqueue(this.componentDidMount, this);
+ if (inst.componentDidMount) {
+ transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
}
return markup;
}
@@ -227,9 +222,11 @@ var ReactCompositeComponentMixin = {
* @internal
*/
unmountComponent: function() {
+ var inst = this._instance;
+
this._compositeLifeCycleState = CompositeLifeCycle.UNMOUNTING;
- if (this.componentWillUnmount) {
- this.componentWillUnmount();
+ if (inst.componentWillUnmount) {
+ inst.componentWillUnmount();
}
this._compositeLifeCycleState = null;
@@ -238,23 +235,23 @@ var ReactCompositeComponentMixin = {
ReactComponent.Mixin.unmountComponent.call(this);
- // Some existing components rely on this.props even after they've been
+ // Delete the reference from the instance to this internal representation
+ // which allow the internals to be properly cleaned up even if the user
+ // leaks a reference to the public instance.
+ ReactInstanceMap.remove(inst);
+
+ // Some existing components rely on inst.props even after they've been
// destroyed (in event handlers).
- // TODO: this.props = null;
- // TODO: this.state = null;
+ // TODO: inst.props = null;
+ // TODO: inst.state = null;
+ // TODO: inst.context = null;
},
/**
- * Sets a subset of the state. Always use this or `replaceState` to mutate
- * state. You should treat `this.state` as immutable.
- *
- * There is no guarantee that `this.state` will be immediately updated, so
- * accessing `this.state` after calling this method may return the old value.
- *
- * There is no guarantee that calls to `setState` will run synchronously,
- * as they may eventually be batched together. You can provide an optional
- * callback that will be executed when the call to setState is actually
- * completed.
+ * Sets a subset of the state. This only exists because _pendingState is
+ * internal. This provides a merging strategy that is not available to deep
+ * properties which is confusing. TODO: Expose pendingState or don't use it
+ * during the merge.
*
* @param {object} partialState Next partial state to be merged with state.
* @param {?function} callback Called after state is updated.
@@ -262,20 +259,9 @@ var ReactCompositeComponentMixin = {
* @protected
*/
setState: function(partialState, callback) {
- invariant(
- typeof partialState === 'object' || partialState == null,
- 'setState(...): takes an object of state variables to update.'
- );
- if (__DEV__){
- warning(
- partialState != null,
- 'setState(...): You passed an undefined or null state object; ' +
- 'instead, use forceUpdate().'
- );
- }
// Merge with `_pendingState` if it exists, otherwise with existing state.
this.replaceState(
- assign({}, this._pendingState || this.state, partialState),
+ assign({}, this._pendingState || this._instance.state, partialState),
callback
);
},
@@ -306,6 +292,32 @@ var ReactCompositeComponentMixin = {
}
},
+ /**
+ * Forces an update. This should only be invoked when it is known with
+ * certainty that we are **not** in a DOM transaction.
+ *
+ * You may want to call this when you know that some deeper aspect of the
+ * component's state has changed but `setState` was not called.
+ *
+ * This will not invoke `shouldUpdateComponent`, but it will invoke
+ * `componentWillUpdate` and `componentDidUpdate`.
+ *
+ * @param {?function} callback Called after update is complete.isM
+ * @final
+ * @protected
+ */
+ forceUpdate: function(callback) {
+ var compositeLifeCycleState = this._compositeLifeCycleState;
+ invariant(
+ compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_STATE &&
+ compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
+ 'forceUpdate(...): Cannot force an update while unmounting component ' +
+ 'or during an existing state transition (such as within `render`).'
+ );
+ this._pendingForceUpdate = true;
+ ReactUpdates.enqueueUpdate(this, callback);
+ },
+
/**
* Filters the context object to only contain keys specified in
* `contextTypes`, and asserts that they are valid.
@@ -316,7 +328,7 @@ var ReactCompositeComponentMixin = {
*/
_processContext: function(context) {
var maskedContext = null;
- var contextTypes = this.constructor.contextTypes;
+ var contextTypes = this._instance.constructor.contextTypes;
if (contextTypes) {
maskedContext = {};
for (var contextName in contextTypes) {
@@ -339,25 +351,26 @@ var ReactCompositeComponentMixin = {
* @private
*/
_processChildContext: function(currentContext) {
- var childContext = this.getChildContext && this.getChildContext();
- var displayName = this.constructor.displayName || 'ReactCompositeComponent';
+ var inst = this._instance;
+ var childContext = inst.getChildContext && inst.getChildContext();
+ var displayName = inst.constructor.displayName || 'ReactCompositeComponent';
if (childContext) {
invariant(
- typeof this.constructor.childContextTypes === 'object',
+ typeof inst.constructor.childContextTypes === 'object',
'%s.getChildContext(): childContextTypes must be defined in order to ' +
'use getChildContext().',
displayName
);
if (__DEV__) {
this._checkPropTypes(
- this.constructor.childContextTypes,
+ inst.constructor.childContextTypes,
childContext,
ReactPropTypeLocations.childContext
);
}
for (var name in childContext) {
invariant(
- name in this.constructor.childContextTypes,
+ name in inst.constructor.childContextTypes,
'%s.getChildContext(): key "%s" is not defined in childContextTypes.',
displayName,
name
@@ -379,7 +392,8 @@ var ReactCompositeComponentMixin = {
*/
_processProps: function(newProps) {
if (__DEV__) {
- var propTypes = this.constructor.propTypes;
+ var inst = this._instance;
+ var propTypes = inst.constructor.propTypes;
if (propTypes) {
this._checkPropTypes(propTypes, newProps, ReactPropTypeLocations.prop);
}
@@ -398,7 +412,7 @@ var ReactCompositeComponentMixin = {
_checkPropTypes: function(propTypes, props, location) {
// TODO: Stop validating prop types here and only use the element
// validation.
- var componentName = this.constructor.displayName;
+ var componentName = this._instance.constructor.displayName;
for (var propName in propTypes) {
if (propTypes.hasOwnProperty(propName)) {
var error =
@@ -497,8 +511,10 @@ var ReactCompositeComponentMixin = {
nextParentElement
);
- var prevContext = this.context;
- var prevProps = this.props;
+ var inst = this._instance;
+
+ var prevContext = inst.context;
+ var prevProps = inst.props;
var nextContext = prevContext;
var nextProps = prevProps;
// Distinguish between a props update versus a simple state update
@@ -507,25 +523,25 @@ var ReactCompositeComponentMixin = {
nextProps = this._processProps(nextParentElement.props);
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
- if (this.componentWillReceiveProps) {
- this.componentWillReceiveProps(nextProps, nextContext);
+ if (inst.componentWillReceiveProps) {
+ inst.componentWillReceiveProps(nextProps, nextContext);
}
}
this._compositeLifeCycleState = null;
- var nextState = this._pendingState || this.state;
+ var nextState = this._pendingState || inst.state;
this._pendingState = null;
var shouldUpdate =
this._pendingForceUpdate ||
- !this.shouldComponentUpdate ||
- this.shouldComponentUpdate(nextProps, nextState, nextContext);
+ !inst.shouldComponentUpdate ||
+ inst.shouldComponentUpdate(nextProps, nextState, nextContext);
if (__DEV__) {
if (typeof shouldUpdate === "undefined") {
console.warn(
- (this.constructor.displayName || 'ReactCompositeComponent') +
+ (inst.constructor.displayName || 'ReactCompositeComponent') +
'.shouldComponentUpdate(): Returned undefined instead of a ' +
'boolean value. Make sure to return true or false.'
);
@@ -536,9 +552,9 @@ var ReactCompositeComponentMixin = {
// If it's determined that a component should not update, we still want
// to set props and state but we shortcut the rest of the update.
this._currentElement = nextParentElement;
- this.props = nextProps;
- this.state = nextState;
- this.context = nextContext;
+ inst.props = nextProps;
+ inst.state = nextState;
+ inst.context = nextContext;
// Owner cannot change because shouldUpdateReactComponent doesn't allow
// it. TODO: Remove this._owner completely.
@@ -576,18 +592,20 @@ var ReactCompositeComponentMixin = {
nextContext,
transaction
) {
- var prevProps = this.props;
- var prevState = this.state;
- var prevContext = this.context;
+ var inst = this._instance;
+
+ var prevProps = inst.props;
+ var prevState = inst.state;
+ var prevContext = inst.context;
- if (this.componentWillUpdate) {
- this.componentWillUpdate(nextProps, nextState, nextContext);
+ if (inst.componentWillUpdate) {
+ inst.componentWillUpdate(nextProps, nextState, nextContext);
}
this._currentElement = nextElement;
- this.props = nextProps;
- this.state = nextState;
- this.context = nextContext;
+ inst.props = nextProps;
+ inst.state = nextState;
+ inst.context = nextContext;
// Owner cannot change because shouldUpdateReactComponent doesn't allow
// it. TODO: Remove this._owner completely.
@@ -595,10 +613,10 @@ var ReactCompositeComponentMixin = {
this._updateRenderedComponent(transaction);
- if (this.componentDidUpdate) {
+ if (inst.componentDidUpdate) {
transaction.getReactMountReady().enqueue(
- this.componentDidUpdate.bind(this, prevProps, prevState, prevContext),
- this
+ inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),
+ inst
);
}
},
@@ -623,7 +641,7 @@ var ReactCompositeComponentMixin = {
var thisID = this._rootNodeID;
var prevComponentID = prevComponentInstance._rootNodeID;
prevComponentInstance.unmountComponent();
- this._renderedComponent = instantiateReactComponent(
+ this._renderedComponent = this._instantiateReactComponent(
nextRenderedElement,
this._currentElement.type
);
@@ -639,38 +657,6 @@ var ReactCompositeComponentMixin = {
}
},
- /**
- * Forces an update. This should only be invoked when it is known with
- * certainty that we are **not** in a DOM transaction.
- *
- * You may want to call this when you know that some deeper aspect of the
- * component's state has changed but `setState` was not called.
- *
- * This will not invoke `shouldUpdateComponent`, but it will invoke
- * `componentWillUpdate` and `componentDidUpdate`.
- *
- * @param {?function} callback Called after update is complete.
- * @final
- * @protected
- */
- forceUpdate: function(callback) {
- var compositeLifeCycleState = this._compositeLifeCycleState;
- invariant(
- this.isMounted() ||
- compositeLifeCycleState === CompositeLifeCycle.MOUNTING,
- 'forceUpdate(...): Can only force an update on mounted or mounting ' +
- 'components.'
- );
- invariant(
- compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING &&
- ReactCurrentOwner.current == null,
- 'forceUpdate(...): Cannot force an update while unmounting component ' +
- 'or within a `render` function.'
- );
- this._pendingForceUpdate = true;
- ReactUpdates.enqueueUpdate(this, callback);
- },
-
/**
* @private
*/
@@ -684,12 +670,13 @@ var ReactCompositeComponentMixin = {
this._currentElement._context
);
ReactCurrentOwner.current = this;
+ var inst = this._instance;
try {
- renderedComponent = this.render();
+ renderedComponent = inst.render();
if (__DEV__) {
// We allow auto-mocks to proceed as if they're returning null.
if (typeof renderedComponent === 'undefined' &&
- this.render._isMockFunction) {
+ inst.render._isMockFunction) {
// This is probably bad practice. Consider warning here and
// deprecating this convenience.
renderedComponent = null;
@@ -709,100 +696,35 @@ var ReactCompositeComponentMixin = {
ReactElement.isValidElement(renderedComponent),
'%s.render(): A valid ReactComponent must be returned. You may have ' +
'returned undefined, an array or some other invalid object.',
- this.constructor.displayName || 'ReactCompositeComponent'
+ inst.constructor.displayName || 'ReactCompositeComponent'
);
return renderedComponent;
}
),
/**
- * @private
+ * Get the publicly accessible representation of this component - i.e. what
+ * is exposed by refs and renderComponent. Can be null for stateless
+ * components.
+ *
+ * @return {ReactComponent} the public component instance.
+ * @internal
*/
- _bindAutoBindMethods: function() {
- for (var autoBindKey in this.__reactAutoBindMap) {
- if (!this.__reactAutoBindMap.hasOwnProperty(autoBindKey)) {
- continue;
- }
- var method = this.__reactAutoBindMap[autoBindKey];
- this[autoBindKey] = this._bindAutoBindMethod(ReactErrorUtils.guard(
- method,
- this.constructor.displayName + '.' + autoBindKey
- ));
- }
+ getPublicInstance: function() {
+ return this._instance;
},
- /**
- * Binds a method to the component.
- *
- * @param {function} method Method to be bound.
- * @private
- */
- _bindAutoBindMethod: function(method) {
- var component = this;
- var boundMethod = method.bind(component);
- if (__DEV__) {
- boundMethod.__reactBoundContext = component;
- boundMethod.__reactBoundMethod = method;
- boundMethod.__reactBoundArguments = null;
- var componentName = component.constructor.displayName;
- var _bind = boundMethod.bind;
- boundMethod.bind = function(newThis, ...args) {
- // User is trying to bind() an autobound method; we effectively will
- // ignore the value of "this" that the user is trying to use, so
- // let's warn.
- if (newThis !== component && newThis !== null) {
- monitorCodeUse('react_bind_warning', { component: componentName });
- console.warn(
- 'bind(): React component methods may only be bound to the ' +
- 'component instance. See ' + componentName
- );
- } else if (!args.length) {
- monitorCodeUse('react_bind_warning', { component: componentName });
- console.warn(
- 'bind(): You are binding a component method to the component. ' +
- 'React does this for you automatically in a high-performance ' +
- 'way, so you can safely remove this call. See ' + componentName
- );
- return boundMethod;
- }
- var reboundMethod = _bind.apply(boundMethod, arguments);
- reboundMethod.__reactBoundContext = component;
- reboundMethod.__reactBoundMethod = method;
- reboundMethod.__reactBoundArguments = args;
- return reboundMethod;
- };
- }
- return boundMethod;
- }
-};
+ // Stub
+ _instantiateReactComponent: null
-var ReactCompositeComponentBase = function() {};
-assign(
- ReactCompositeComponentBase.prototype,
- ReactComponent.Mixin,
- ReactOwner.Mixin,
- ReactPropTransferer.Mixin,
- ReactCompositeComponentMixin
-);
+});
-/**
- * Module for creating composite components.
- *
- * @class ReactCompositeComponent
- * @extends ReactComponent
- * @extends ReactOwner
- * @extends ReactPropTransferer
- */
var ReactCompositeComponent = {
LifeCycle: CompositeLifeCycle,
- Base: ReactCompositeComponentBase
+ Mixin: ReactCompositeComponentMixin
};
-// Temporary injection.
-// TODO: Delete this hack once implementation details are hidden.
-instantiateReactComponent._compositeBase = ReactCompositeComponentBase;
-
module.exports = ReactCompositeComponent;
diff --git a/src/core/ReactElementValidator.js b/src/core/ReactElementValidator.js
index 0469e8ca45fd7..2dacfd5c786a3 100644
--- a/src/core/ReactElementValidator.js
+++ b/src/core/ReactElementValidator.js
@@ -48,7 +48,9 @@ var NUMERIC_PROPERTY_REGEX = /^\d+$/;
*/
function getCurrentOwnerDisplayName() {
var current = ReactCurrentOwner.current;
- return current && current.constructor.displayName || undefined;
+ return (
+ current && current.getPublicInstance().constructor.displayName || undefined
+ );
}
/**
@@ -128,7 +130,8 @@ function warnAndMonitorForKeyUse(warningID, message, component, parentType) {
component._owner &&
component._owner !== ReactCurrentOwner.current) {
// Name of the component that originally created this child.
- childOwnerName = component._owner.constructor.displayName;
+ childOwnerName =
+ component._owner.getPublicInstance().constructor.displayName;
message += ` It was passed a child from ${childOwnerName}.`;
}
diff --git a/src/core/ReactInstanceMap.js b/src/core/ReactInstanceMap.js
new file mode 100644
index 0000000000000..6c6209eb947b6
--- /dev/null
+++ b/src/core/ReactInstanceMap.js
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2013-2014, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ReactInstanceMap
+ */
+
+"use strict";
+
+/**
+ * `ReactInstanceMap` maintains a mapping from a public facing stateful
+ * instance (key) and the internal representation (value). This allows public
+ * methods to accept the user facing instance as an argument and map them back
+ * to internal methods.
+ */
+
+// TODO: Replace this with ES6: var ReactInstanceMap = new Map();
+var ReactInstanceMap = {
+
+ /**
+ * This API should be called `delete` but we'd have to make sure to always
+ * transform these to strings for IE support. When this transform is fully
+ * supported we can rename it.
+ */
+ remove: function(key) {
+ key._reactInternalInstance = undefined;
+ },
+
+ get: function(key) {
+ return key._reactInternalInstance;
+ },
+
+ has: function(key) {
+ return key._reactInternalInstance !== undefined;
+ },
+
+ set: function(key, value) {
+ key._reactInternalInstance = value;
+ }
+
+};
+
+module.exports = ReactInstanceMap;
diff --git a/src/core/ReactOwner.js b/src/core/ReactOwner.js
index 36f155940dec4..22e493493e7d7 100644
--- a/src/core/ReactOwner.js
+++ b/src/core/ReactOwner.js
@@ -100,7 +100,7 @@ var ReactOwner = {
);
// Check that `component` is still the current ref because we do not want to
// detach the ref if another component stole it.
- if (owner.refs[ref] === component) {
+ if (owner.getPublicInstance().refs[ref] === component.getPublicInstance()) {
owner.detachRef(ref);
}
},
@@ -113,7 +113,8 @@ var ReactOwner = {
Mixin: {
construct: function() {
- this.refs = emptyObject;
+ var inst = this.getPublicInstance();
+ inst.refs = emptyObject;
},
/**
@@ -125,13 +126,16 @@ var ReactOwner = {
* @private
*/
attachRef: function(ref, component) {
+ // TODO: Remove this invariant. This is never exposed and cannot be called
+ // by user code. The unit test is already removed.
invariant(
component.isOwnedBy(this),
'attachRef(%s, ...): Only a component\'s owner can store a ref to it.',
ref
);
- var refs = this.refs === emptyObject ? (this.refs = {}) : this.refs;
- refs[ref] = component;
+ var inst = this.getPublicInstance();
+ var refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs;
+ refs[ref] = component.getPublicInstance();
},
/**
@@ -142,7 +146,8 @@ var ReactOwner = {
* @private
*/
detachRef: function(ref) {
- delete this.refs[ref];
+ var refs = this.getPublicInstance().refs;
+ delete refs[ref];
}
}
diff --git a/src/core/ReactPropTransferer.js b/src/core/ReactPropTransferer.js
index ad2f93413d677..01a4591725230 100644
--- a/src/core/ReactPropTransferer.js
+++ b/src/core/ReactPropTransferer.js
@@ -129,7 +129,7 @@ var ReactPropTransferer = {
*/
transferPropsTo: function(element) {
invariant(
- element._owner === this,
+ element._owner && element._owner.getPublicInstance() === this,
'%s: You can\'t call transferPropsTo() on a component that you ' +
'don\'t own, %s. This usually means you are calling ' +
'transferPropsTo() on a component passed in as props or children.',
diff --git a/src/core/ReactRef.js b/src/core/ReactRef.js
index 4463d54bf0fc2..b93290703ef3d 100644
--- a/src/core/ReactRef.js
+++ b/src/core/ReactRef.js
@@ -89,7 +89,7 @@ assign(ReactRef.prototype, {
});
ReactRef.attachRef = function(ref, value) {
- ref._value = value;
+ ref._value = value.getPublicInstance();
};
ReactRef.detachRef = function(ref, value) {
diff --git a/src/core/__tests__/ReactComponent-test.js b/src/core/__tests__/ReactComponent-test.js
index 3f4129b72a755..35e7ada062a32 100644
--- a/src/core/__tests__/ReactComponent-test.js
+++ b/src/core/__tests__/ReactComponent-test.js
@@ -12,15 +12,26 @@
"use strict";
var React;
+var ReactInstanceMap;
var ReactTestUtils;
var reactComponentExpect;
+var getMountDepth;
describe('ReactComponent', function() {
beforeEach(function() {
React = require('React');
+ ReactInstanceMap = require('ReactInstanceMap');
ReactTestUtils = require('ReactTestUtils');
reactComponentExpect = require('reactComponentExpect');
+
+ getMountDepth = function(instance) {
+ if (instance.mountComponent) {
+ // Native instance
+ return instance._mountDepth;
+ }
+ return ReactInstanceMap.get(instance)._mountDepth;
+ };
});
it('should throw on invalid render targets', function() {
@@ -48,26 +59,6 @@ describe('ReactComponent', function() {
}).toThrow();
});
- it('should throw when attempting to hijack a ref', function() {
- var Component = React.createClass({
- render: function() {
- var child = this.props.child;
- this.attachRef('test', child);
- return child;
- }
- });
-
- var childInstance = ReactTestUtils.renderIntoDocument();
- var instance = ;
-
- expect(function() {
- instance = ReactTestUtils.renderIntoDocument(instance);
- }).toThrow(
- 'Invariant Violation: attachRef(test, ...): Only a component\'s owner ' +
- 'can store a ref to it.'
- );
- });
-
it('should support refs on owned components', function() {
var innerObj = {}, outerObj = {};
@@ -260,8 +251,8 @@ describe('ReactComponent', function() {
var instance = ;
instance = ReactTestUtils.renderIntoDocument(instance);
- expect(instance._mountDepth).toBe(0);
- expect(instance.refs.child._mountDepth).toBe(1);
+ expect(getMountDepth(instance)).toBe(0);
+ expect(getMountDepth(instance.refs.child)).toBe(1);
});
it('should know its (complicated) mount depth', function() {
@@ -291,7 +282,7 @@ describe('ReactComponent', function() {
ref="switcherDiv"
style={{
display: this.state.tabKey === child.key ? '' : 'none'
- }}>
+ }}>
{child}
@@ -312,12 +303,12 @@ describe('ReactComponent', function() {
var root = ;
root = ReactTestUtils.renderIntoDocument(root);
- expect(root._mountDepth).toBe(0);
- expect(root.refs.switcher._mountDepth).toBe(1);
- expect(root.refs.switcher.refs.box._mountDepth).toBe(2);
- expect(root.refs.switcher.refs.switcherDiv._mountDepth).toBe(4);
- expect(root.refs.child._mountDepth).toBe(5);
- expect(root.refs.switcher.refs.box.refs.boxDiv._mountDepth).toBe(3);
- expect(root.refs.child.refs.span._mountDepth).toBe(6);
+ expect(getMountDepth(root)).toBe(0);
+ expect(getMountDepth(root.refs.switcher)).toBe(1);
+ expect(getMountDepth(root.refs.switcher.refs.box)).toBe(2);
+ expect(getMountDepth(root.refs.switcher.refs.switcherDiv)).toBe(4);
+ expect(getMountDepth(root.refs.child)).toBe(5);
+ expect(getMountDepth(root.refs.switcher.refs.box.refs.boxDiv)).toBe(3);
+ expect(getMountDepth(root.refs.child.refs.span)).toBe(6);
});
});
diff --git a/src/core/__tests__/ReactComponentLifeCycle-test.js b/src/core/__tests__/ReactComponentLifeCycle-test.js
index a625bf2401786..214b18d017206 100644
--- a/src/core/__tests__/ReactComponentLifeCycle-test.js
+++ b/src/core/__tests__/ReactComponentLifeCycle-test.js
@@ -15,9 +15,13 @@ var React;
var ReactTestUtils;
var ReactComponent;
var ReactCompositeComponent;
+var ReactInstanceMap;
var ComponentLifeCycle;
var CompositeComponentLifeCycle;
+var getCompositeLifeCycle;
+var getLifeCycleState;
+
var clone = function(o) {
return JSON.parse(JSON.stringify(o));
};
@@ -81,6 +85,23 @@ describe('ReactComponentLifeCycle', function() {
ReactCompositeComponent = require('ReactCompositeComponent');
ComponentLifeCycle = ReactComponent.LifeCycle;
CompositeComponentLifeCycle = ReactCompositeComponent.LifeCycle;
+
+ ReactInstanceMap = require('ReactInstanceMap');
+
+ getCompositeLifeCycle = function(instance) {
+ return ReactInstanceMap.get(instance)._compositeLifeCycleState;
+ };
+
+ getLifeCycleState = function(instance) {
+ var internalInstance = ReactInstanceMap.get(instance);
+ if (!internalInstance) {
+ // Once a component is fully unmounted, we cannot actually get to the
+ // internal instance. It's already dereferenced and possibly GC:ed.
+ // So the unmounted life cycle hook doesn't exist anymore.
+ return ComponentLifeCycle.UNMOUNTED;
+ }
+ return internalInstance._lifeCycleState;
+ };
});
it('should not reuse an instance when it has been unmounted', function() {
@@ -222,25 +243,25 @@ describe('ReactComponentLifeCycle', function() {
};
this._testJournal.returnedFromGetInitialState = clone(initState);
this._testJournal.lifeCycleAtStartOfGetInitialState =
- this._lifeCycleState;
+ getLifeCycleState(this);
this._testJournal.compositeLifeCycleAtStartOfGetInitialState =
- this._compositeLifeCycleState;
+ getCompositeLifeCycle(this);
return initState;
},
componentWillMount: function() {
this._testJournal.stateAtStartOfWillMount = clone(this.state);
this._testJournal.lifeCycleAtStartOfWillMount =
- this._lifeCycleState;
+ getLifeCycleState(this);
this._testJournal.compositeLifeCycleAtStartOfWillMount =
- this._compositeLifeCycleState;
+ getCompositeLifeCycle(this);
this.state.hasWillMountCompleted = true;
},
componentDidMount: function() {
this._testJournal.stateAtStartOfDidMount = clone(this.state);
this._testJournal.lifeCycleAtStartOfDidMount =
- this._lifeCycleState;
+ getLifeCycleState(this);
this.setState({hasDidMountCompleted: true});
},
@@ -248,12 +269,12 @@ describe('ReactComponentLifeCycle', function() {
var isInitialRender = !this.state.hasRenderCompleted;
if (isInitialRender) {
this._testJournal.stateInInitialRender = clone(this.state);
- this._testJournal.lifeCycleInInitialRender = this._lifeCycleState;
+ this._testJournal.lifeCycleInInitialRender = getLifeCycleState(this);
this._testJournal.compositeLifeCycleInInitialRender =
- this._compositeLifeCycleState;
+ getCompositeLifeCycle(this);
} else {
this._testJournal.stateInLaterRender = clone(this.state);
- this._testJournal.lifeCycleInLaterRender = this._lifeCycleState;
+ this._testJournal.lifeCycleInLaterRender = getLifeCycleState(this);
}
// you would *NEVER* do anything like this in real code!
this.state.hasRenderCompleted = true;
@@ -267,7 +288,7 @@ describe('ReactComponentLifeCycle', function() {
componentWillUnmount: function() {
this._testJournal.stateAtStartOfWillUnmount = clone(this.state);
this._testJournal.lifeCycleAtStartOfWillUnmount =
- this._lifeCycleState;
+ getLifeCycleState(this);
this.state.hasWillUnmountCompleted = true;
}
});
@@ -275,7 +296,8 @@ describe('ReactComponentLifeCycle', function() {
// A component that is merely "constructed" (as in "constructor") but not
// yet initialized, or rendered.
//
- var instance = ReactTestUtils.renderIntoDocument();
+ var container = document.createElement('div');
+ var instance = React.render(, container);
// getInitialState
expect(instance._testJournal.returnedFromGetInitialState).toEqual(
@@ -312,7 +334,7 @@ describe('ReactComponentLifeCycle', function() {
CompositeComponentLifeCycle.MOUNTING
);
- expect(instance._lifeCycleState).toBe(ComponentLifeCycle.MOUNTED);
+ expect(getLifeCycleState(instance)).toBe(ComponentLifeCycle.MOUNTED);
// Now *update the component*
instance.forceUpdate();
@@ -324,10 +346,9 @@ describe('ReactComponentLifeCycle', function() {
ComponentLifeCycle.MOUNTED
);
- expect(instance._lifeCycleState).toBe(ComponentLifeCycle.MOUNTED);
+ expect(getLifeCycleState(instance)).toBe(ComponentLifeCycle.MOUNTED);
- // Now *unmountComponent*
- instance.unmountComponent();
+ React.unmountComponentAtNode(container);
expect(instance._testJournal.stateAtStartOfWillUnmount)
.toEqual(WILL_UNMOUNT_STATE);
@@ -337,7 +358,7 @@ describe('ReactComponentLifeCycle', function() {
);
// But the current lifecycle of the component is unmounted.
- expect(instance._lifeCycleState).toBe(ComponentLifeCycle.UNMOUNTED);
+ expect(getLifeCycleState(instance)).toBe(ComponentLifeCycle.UNMOUNTED);
expect(instance.state).toEqual(POST_WILL_UNMOUNT_STATE);
});
@@ -487,10 +508,11 @@ describe('ReactComponentLifeCycle', function() {
componentDidUpdate: logger('inner componentDidUpdate'),
componentWillUnmount: logger('inner componentWillUnmount')
});
- var instance;
+
+ var container = document.createElement('div');
log = [];
- instance = ReactTestUtils.renderIntoDocument();
+ var instance = React.render(, container);
expect(log).toEqual([
'outer componentWillMount',
'inner componentWillMount',
@@ -512,7 +534,7 @@ describe('ReactComponentLifeCycle', function() {
]);
log = [];
- instance.unmountComponent();
+ React.unmountComponentAtNode(container);
expect(log).toEqual([
'outer componentWillUnmount',
'inner componentWillUnmount'
diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js
index cf94ce86b88c0..fbe792596f189 100644
--- a/src/core/__tests__/ReactCompositeComponent-test.js
+++ b/src/core/__tests__/ReactCompositeComponent-test.js
@@ -623,6 +623,63 @@ describe('ReactCompositeComponent', function() {
);
});
+ it('should not allow `setState` on unmounted components', function() {
+ var container = document.createElement('div');
+ document.documentElement.appendChild(container);
+
+ var Component = React.createClass({
+ getInitialState: function() {
+ return { value: 0 };
+ },
+ render: function() {
+ return ;
+ }
+ });
+
+ var instance = ;
+ expect(instance.setState).not.toBeDefined();
+
+ instance = React.render(instance, container);
+ expect(function() {
+ instance.setState({ value: 1 });
+ }).not.toThrow();
+
+ React.unmountComponentAtNode(container);
+ expect(function() {
+ instance.setState({ value: 2 });
+ }).toThrow(
+ 'Invariant Violation: setState(...): Can only update a mounted or ' +
+ 'mounting component.'
+ );
+ });
+
+ it('should not allow `setProps` on unmounted components', function() {
+ var container = document.createElement('div');
+ document.documentElement.appendChild(container);
+
+ var Component = React.createClass({
+ render: function() {
+ return ;
+ }
+ });
+
+ var instance = ;
+ expect(instance.setProps).not.toBeDefined();
+
+ instance = React.render(instance, container);
+ expect(function() {
+ instance.setProps({ value: 1 });
+ }).not.toThrow();
+
+ React.unmountComponentAtNode(container);
+ expect(function() {
+ instance.setProps({ value: 2 });
+ }).toThrow(
+ 'Invariant Violation: setProps(...): Can only update a mounted ' +
+ 'component.'
+ );
+ });
+
it('should cleanup even if render() fatals', function() {
var BadComponent = React.createClass({
render: function() {
diff --git a/src/core/__tests__/ReactCompositeComponentState-test.js b/src/core/__tests__/ReactCompositeComponentState-test.js
index 9de786fbac432..10fa09a1bf929 100644
--- a/src/core/__tests__/ReactCompositeComponentState-test.js
+++ b/src/core/__tests__/ReactCompositeComponentState-test.js
@@ -14,6 +14,7 @@
var mocks = require('mocks');
var React;
+var ReactInstanceMap;
var ReactTestUtils;
var reactComponentExpect;
@@ -23,6 +24,7 @@ describe('ReactCompositeComponent-state', function() {
beforeEach(function() {
React = require('React');
+ ReactInstanceMap = require('ReactInstanceMap');
ReactTestUtils = require('ReactTestUtils');
reactComponentExpect = require('reactComponentExpect');
@@ -31,10 +33,13 @@ describe('ReactCompositeComponent-state', function() {
if (state) {
this.props.stateListener(from, state && state.color);
} else {
+ var internalInstance = ReactInstanceMap.get(this);
+ var pendingState = internalInstance ? internalInstance._pendingState :
+ null;
this.props.stateListener(
from,
this.state && this.state.color,
- this._pendingState && this._pendingState.color
+ pendingState && pendingState.color
);
}
},
@@ -99,13 +104,17 @@ describe('ReactCompositeComponent-state', function() {
});
it('should support setting state', function() {
+ var container = document.createElement('div');
+ document.documentElement.appendChild(container);
+
var stateListener = mocks.getMockFunction();
var instance = ;
- instance = ReactTestUtils.renderIntoDocument(instance);
+ instance = React.render(instance, container);
instance.setProps({nextColor: 'green'});
instance.setFavoriteColor('blue');
instance.forceUpdate();
- instance.unmountComponent();
+
+ React.unmountComponentAtNode(container);
expect(stateListener.mock.calls).toEqual([
// there is no state when getInitialState() is called
diff --git a/src/core/__tests__/ReactElement-test.js b/src/core/__tests__/ReactElement-test.js
index ba5af2e77ab83..76255209ddb1a 100644
--- a/src/core/__tests__/ReactElement-test.js
+++ b/src/core/__tests__/ReactElement-test.js
@@ -138,7 +138,7 @@ describe('ReactElement', function() {
var instance = ReactTestUtils.renderIntoDocument();
- expect(element._owner).toBe(instance);
+ expect(element._owner.getPublicInstance()).toBe(instance);
});
it('merges an additional argument onto the children prop', function() {
diff --git a/src/core/__tests__/ReactInstanceHandles-test.js b/src/core/__tests__/ReactInstanceHandles-test.js
index 82e835282d23f..0620b40d21291 100644
--- a/src/core/__tests__/ReactInstanceHandles-test.js
+++ b/src/core/__tests__/ReactInstanceHandles-test.js
@@ -62,6 +62,14 @@ describe('ReactInstanceHandles', function() {
});
}
+ function getNodeID(instance) {
+ if (instance === null) {
+ return '';
+ }
+ var internal = ReactTestUtils.getInternalRepresentation(instance);
+ return internal._rootNodeID;
+ }
+
beforeEach(function() {
ReactInstanceHandles = require('ReactInstanceHandles');
aggregatedArgs = [];
@@ -178,17 +186,17 @@ describe('ReactInstanceHandles', function() {
it("should traverse two phase across component boundary", function() {
var parent = renderParentIntoDocument();
- var targetID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
+ var targetID = getNodeID(parent.refs.P_P1_C1.refs.DIV_1);
var expectedAggregation = [
- {id: parent.refs.P._rootNodeID, isUp: false, arg: ARG},
- {id: parent.refs.P_P1._rootNodeID, isUp: false, arg: ARG},
- {id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: false, arg: ARG},
- {id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: false, arg: ARG},
-
- {id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: true, arg: ARG},
- {id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: true, arg: ARG},
- {id: parent.refs.P_P1._rootNodeID, isUp: true, arg: ARG},
- {id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
+ {id: getNodeID(parent.refs.P), isUp: false, arg: ARG},
+ {id: getNodeID(parent.refs.P_P1), isUp: false, arg: ARG},
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: false, arg: ARG},
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV_1), isUp: false, arg: ARG},
+
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV_1), isUp: true, arg: ARG},
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: true, arg: ARG},
+ {id: getNodeID(parent.refs.P_P1), isUp: true, arg: ARG},
+ {id: getNodeID(parent.refs.P), isUp: true, arg: ARG}
];
ReactInstanceHandles.traverseTwoPhase(targetID, argAggregator, ARG);
expect(aggregatedArgs).toEqual(expectedAggregation);
@@ -196,10 +204,10 @@ describe('ReactInstanceHandles', function() {
it("should traverse two phase at shallowest node", function() {
var parent = renderParentIntoDocument();
- var targetID = parent.refs.P._rootNodeID;
+ var targetID = getNodeID(parent.refs.P);
var expectedAggregation = [
- {id: parent.refs.P._rootNodeID, isUp: false, arg: ARG},
- {id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
+ {id: getNodeID(parent.refs.P), isUp: false, arg: ARG},
+ {id: getNodeID(parent.refs.P), isUp: true, arg: ARG}
];
ReactInstanceHandles.traverseTwoPhase(targetID, argAggregator, ARG);
expect(aggregatedArgs).toEqual(expectedAggregation);
@@ -218,8 +226,8 @@ describe('ReactInstanceHandles', function() {
it("should not traverse if enter/leave the same node", function() {
var parent = renderParentIntoDocument();
- var leaveID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
- var enterID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
+ var leaveID = getNodeID(parent.refs.P_P1_C1.refs.DIV_1);
+ var enterID = getNodeID(parent.refs.P_P1_C1.refs.DIV_1);
var expectedAggregation = [];
ReactInstanceHandles.traverseEnterLeave(
leaveID, enterID, argAggregator, ARG, ARG2
@@ -229,12 +237,12 @@ describe('ReactInstanceHandles', function() {
it("should traverse enter/leave to sibling - avoids parent", function() {
var parent = renderParentIntoDocument();
- var leaveID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
- var enterID = parent.refs.P_P1_C1.refs.DIV_2._rootNodeID;
+ var leaveID = getNodeID(parent.refs.P_P1_C1.refs.DIV_1);
+ var enterID = getNodeID(parent.refs.P_P1_C1.refs.DIV_2);
var expectedAggregation = [
- {id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: true, arg: ARG},
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV_1), isUp: true, arg: ARG},
// enter/leave shouldn't fire antyhing on the parent
- {id: parent.refs.P_P1_C1.refs.DIV_2._rootNodeID, isUp: false, arg: ARG2}
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV_2), isUp: false, arg: ARG2}
];
ReactInstanceHandles.traverseEnterLeave(
leaveID, enterID, argAggregator, ARG, ARG2
@@ -244,10 +252,10 @@ describe('ReactInstanceHandles', function() {
it("should traverse enter/leave to parent - avoids parent", function() {
var parent = renderParentIntoDocument();
- var leaveID = parent.refs.P_P1_C1.refs.DIV_1._rootNodeID;
- var enterID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
+ var leaveID = getNodeID(parent.refs.P_P1_C1.refs.DIV_1);
+ var enterID = getNodeID(parent.refs.P_P1_C1.refs.DIV);
var expectedAggregation = [
- {id: parent.refs.P_P1_C1.refs.DIV_1._rootNodeID, isUp: true, arg: ARG}
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV_1), isUp: true, arg: ARG}
];
ReactInstanceHandles.traverseEnterLeave(
leaveID, enterID, argAggregator, ARG, ARG2
@@ -258,11 +266,11 @@ describe('ReactInstanceHandles', function() {
it("should enter from the window", function() {
var parent = renderParentIntoDocument();
var leaveID = ''; // From the window or outside of the React sandbox.
- var enterID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
+ var enterID = getNodeID(parent.refs.P_P1_C1.refs.DIV);
var expectedAggregation = [
- {id: parent.refs.P._rootNodeID, isUp: false, arg: ARG2},
- {id: parent.refs.P_P1._rootNodeID, isUp: false, arg: ARG2},
- {id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: false, arg: ARG2}
+ {id: getNodeID(parent.refs.P), isUp: false, arg: ARG2},
+ {id: getNodeID(parent.refs.P_P1), isUp: false, arg: ARG2},
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: false, arg: ARG2}
];
ReactInstanceHandles.traverseEnterLeave(
leaveID, enterID, argAggregator, ARG, ARG2
@@ -273,9 +281,9 @@ describe('ReactInstanceHandles', function() {
it("should enter from the window to the shallowest", function() {
var parent = renderParentIntoDocument();
var leaveID = ''; // From the window or outside of the React sandbox.
- var enterID = parent.refs.P._rootNodeID;
+ var enterID = getNodeID(parent.refs.P);
var expectedAggregation = [
- {id: parent.refs.P._rootNodeID, isUp: false, arg: ARG2}
+ {id: getNodeID(parent.refs.P), isUp: false, arg: ARG2}
];
ReactInstanceHandles.traverseEnterLeave(
leaveID, enterID, argAggregator, ARG, ARG2
@@ -286,11 +294,11 @@ describe('ReactInstanceHandles', function() {
it("should leave to the window", function() {
var parent = renderParentIntoDocument();
var enterID = ''; // From the window or outside of the React sandbox.
- var leaveID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
+ var leaveID = getNodeID(parent.refs.P_P1_C1.refs.DIV);
var expectedAggregation = [
- {id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: true, arg: ARG},
- {id: parent.refs.P_P1._rootNodeID, isUp: true, arg: ARG},
- {id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: true, arg: ARG},
+ {id: getNodeID(parent.refs.P_P1), isUp: true, arg: ARG},
+ {id: getNodeID(parent.refs.P), isUp: true, arg: ARG}
];
ReactInstanceHandles.traverseEnterLeave(
leaveID, enterID, argAggregator, ARG, ARG2
@@ -301,11 +309,11 @@ describe('ReactInstanceHandles', function() {
it("should leave to the window from the shallowest", function() {
var parent = renderParentIntoDocument();
var enterID = ''; // From the window or outside of the React sandbox.
- var leaveID = parent.refs.P_P1_C1.refs.DIV._rootNodeID;
+ var leaveID = getNodeID(parent.refs.P_P1_C1.refs.DIV);
var expectedAggregation = [
- {id: parent.refs.P_P1_C1.refs.DIV._rootNodeID, isUp: true, arg: ARG},
- {id: parent.refs.P_P1._rootNodeID, isUp: true, arg: ARG},
- {id: parent.refs.P._rootNodeID, isUp: true, arg: ARG}
+ {id: getNodeID(parent.refs.P_P1_C1.refs.DIV), isUp: true, arg: ARG},
+ {id: getNodeID(parent.refs.P_P1), isUp: true, arg: ARG},
+ {id: getNodeID(parent.refs.P), isUp: true, arg: ARG}
];
ReactInstanceHandles.traverseEnterLeave(
leaveID, enterID, argAggregator, ARG, ARG2
@@ -320,9 +328,9 @@ describe('ReactInstanceHandles', function() {
expect(
ReactInstanceHandles._getNextDescendantID(
'',
- parent.refs.P_P1._rootNodeID
+ getNodeID(parent.refs.P_P1)
)
- ).toBe(parent.refs.P._rootNodeID);
+ ).toBe(getNodeID(parent.refs.P));
});
it("should return window for next descendent towards window", function() {
@@ -333,10 +341,10 @@ describe('ReactInstanceHandles', function() {
var parent = renderParentIntoDocument();
expect(
ReactInstanceHandles._getNextDescendantID(
- parent.refs.P_P1._rootNodeID,
- parent.refs.P_P1._rootNodeID
+ getNodeID(parent.refs.P_P1),
+ getNodeID(parent.refs.P_P1)
)
- ).toBe(parent.refs.P_P1._rootNodeID);
+ ).toBe(getNodeID(parent.refs.P_P1));
});
});
@@ -345,19 +353,19 @@ describe('ReactInstanceHandles', function() {
var parent = renderParentIntoDocument();
var ancestors = [
// Common ancestor from window to deep element is ''.
- { one: {_rootNodeID: ''},
+ { one: null,
two: parent.refs.P_P1_C1.refs.DIV_1,
- com: {_rootNodeID: ''}
+ com: null
},
// Same as previous - reversed direction.
{ one: parent.refs.P_P1_C1.refs.DIV_1,
- two: {_rootNodeID: ''},
- com: {_rootNodeID: ''}
+ two: null,
+ com: null
},
// Common ancestor from window to shallow id is ''.
{ one: parent.refs.P,
- two: {_rootNodeID: ''},
- com: {_rootNodeID: ''}
+ two: null,
+ com: null
},
// Common ancestor with self is self.
{ one: parent.refs.P_P1_C1.refs.DIV_1,
@@ -401,10 +409,10 @@ describe('ReactInstanceHandles', function() {
for (i = 0; i < ancestors.length; i++) {
var plan = ancestors[i];
var firstCommon = ReactInstanceHandles._getFirstCommonAncestorID(
- plan.one._rootNodeID,
- plan.two._rootNodeID
+ getNodeID(plan.one),
+ getNodeID(plan.two)
);
- expect(firstCommon).toBe(plan.com._rootNodeID);
+ expect(firstCommon).toBe(getNodeID(plan.com));
}
});
});
diff --git a/src/core/__tests__/ReactMultiChildReconcile-test.js b/src/core/__tests__/ReactMultiChildReconcile-test.js
index babe8616c2db7..faa9d467efa31 100644
--- a/src/core/__tests__/ReactMultiChildReconcile-test.js
+++ b/src/core/__tests__/ReactMultiChildReconcile-test.js
@@ -14,6 +14,7 @@
require('mock-modules');
var React = require('React');
+var ReactInstanceMap = require('ReactInstanceMap');
var ReactTestUtils = require('ReactTestUtils');
var ReactMount = require('ReactMount');
@@ -83,7 +84,10 @@ var FriendsStatusDisplay = React.createClass({
getStatusDisplays: function() {
var name;
var orderOfUsernames = [];
- var statusDisplays = this._renderedComponent._renderedChildren;
+ // TODO: Update this to a better test that doesn't rely so much on internal
+ // implementation details.
+ var statusDisplays =
+ ReactInstanceMap.get(this)._renderedComponent._renderedChildren;
for (name in statusDisplays) {
var child = statusDisplays[name];
var isPresent = !!child;
@@ -195,7 +199,7 @@ function verifyDomOrderingAccurate(parentInstance, statusDisplays) {
continue;
}
var statusDisplay = statusDisplays[username];
- orderedLogicalIds.push(statusDisplay._rootNodeID);
+ orderedLogicalIds.push(ReactInstanceMap.get(statusDisplay)._rootNodeID);
}
expect(orderedDomIds).toEqual(orderedLogicalIds);
}
diff --git a/src/core/__tests__/refs-destruction-test.js b/src/core/__tests__/refs-destruction-test.js
index 4bc95c5a20924..fc1ff60108d45 100644
--- a/src/core/__tests__/refs-destruction-test.js
+++ b/src/core/__tests__/refs-destruction-test.js
@@ -19,9 +19,11 @@ var TestComponent = React.createClass({
render: function() {
return (
-
- Lets try to destroy this.
-
+ {this.props.destroy ? null :
+
+ Lets try to destroy this.
+
+ }
);
}
@@ -33,20 +35,22 @@ describe('refs-destruction', function() {
});
it("should remove refs when destroying the parent", function() {
- var testInstance = ReactTestUtils.renderIntoDocument();
+ var container = document.createElement('div');
+ var testInstance = React.render(, container);
reactComponentExpect(testInstance.refs.theInnerDiv)
.toBeDOMComponentWithTag('div');
expect(Object.keys(testInstance.refs || {}).length).toEqual(1);
- testInstance.unmountComponent();
+ React.unmountComponentAtNode(container);
expect(Object.keys(testInstance.refs || {}).length).toEqual(0);
});
it("should remove refs when destroying the child", function() {
- var testInstance = ReactTestUtils.renderIntoDocument();
+ var container = document.createElement('div');
+ var testInstance = React.render(, container);
reactComponentExpect(testInstance.refs.theInnerDiv)
.toBeDOMComponentWithTag('div');
expect(Object.keys(testInstance.refs || {}).length).toEqual(1);
- testInstance.refs.theInnerDiv.unmountComponent();
+ React.render(, container);
expect(Object.keys(testInstance.refs || {}).length).toEqual(0);
});
});
diff --git a/src/core/instantiateReactComponent.js b/src/core/instantiateReactComponent.js
index 3db5ba3a88a76..e342043ac9e02 100644
--- a/src/core/instantiateReactComponent.js
+++ b/src/core/instantiateReactComponent.js
@@ -12,30 +12,37 @@
"use strict";
-var warning = require('warning');
-
+var ReactCompositeComponent = require('ReactCompositeComponent');
var ReactNativeComponent = require('ReactNativeComponent');
-// This is temporary until we've hidden all the implementation details
-// TODO: Delete this hack once implementation details are hidden
-var publicAPIs = {
- forceUpdate: true,
- replaceState: true,
- setProps: true,
- setState: true,
- getDOMNode: true
- // Public APIs used internally:
- // isMounted: true,
- // replaceProps: true,
-};
+var assign = require('Object.assign');
+var warning = require('warning');
-function unmockImplementationDetails(mockInstance) {
- var ReactCompositeComponentBase = instantiateReactComponent._compositeBase;
- for (var key in ReactCompositeComponentBase.prototype) {
- if (!publicAPIs.hasOwnProperty(key)) {
- mockInstance[key] = ReactCompositeComponentBase.prototype[key];
- }
+// To avoid a cyclic dependency, we create the final class in this module
+var ReactCompositeComponentWrapper = function(inst) {
+ this._instance = inst;
+};
+assign(
+ ReactCompositeComponentWrapper.prototype,
+ ReactCompositeComponent.Mixin,
+ {
+ _instantiateReactComponent: instantiateReactComponent
}
+);
+
+/**
+ * Check if the type reference is a known internal type. I.e. not a user
+ * provided composite type.
+ *
+ * @param {function} type
+ * @return {boolean} Returns true if this is a valid internal type.
+ */
+function isInternalComponentType(type) {
+ return (
+ typeof type === 'function' &&
+ typeof type.prototype.mountComponent === 'function' &&
+ typeof type.prototype.receiveComponent === 'function'
+ );
}
/**
@@ -52,7 +59,7 @@ function instantiateReactComponent(element, parentCompositeType) {
if (__DEV__) {
warning(
element && (typeof element.type === 'function' ||
- typeof element.type === 'string'),
+ typeof element.type === 'string'),
'Only functions or strings can be mounted as React components.'
);
}
@@ -64,17 +71,25 @@ function instantiateReactComponent(element, parentCompositeType) {
element.props,
parentCompositeType
);
+ // If the injected special class is not an internal class, but another
+ // composite, then we must wrap it.
+ // TODO: Move this resolution around to something cleaner.
+ if (typeof instance.mountComponent !== 'function') {
+ instance = new ReactCompositeComponentWrapper(instance);
+ }
+ } else if (isInternalComponentType(element.type)) {
+ // This is temporarily available for custom components that are not string
+ // represenations. I.e. ART. Once those are updated to use the string
+ // representation, we can drop this code path.
+ instance = new element.type(element);
} else {
- // Normal case for non-mocks and non-strings
- instance = new element.type(element.props);
+ // TODO: Update to follow new ES6 initialization. Ideally, we can use props
+ // in property initializers.
+ var inst = new element.type(element.props);
+ instance = new ReactCompositeComponentWrapper(inst);
}
if (__DEV__) {
- if (element.type._isMockFunction) {
- // TODO: Remove this special case
- unmockImplementationDetails(instance);
- }
-
warning(
typeof instance.construct === 'function' &&
typeof instance.mountComponent === 'function' &&
@@ -83,8 +98,7 @@ function instantiateReactComponent(element, parentCompositeType) {
);
}
- // This actually sets up the internal instance. This will become decoupled
- // from the public instance in a future diff.
+ // Sets up the instance. This can probably just move into the constructor now.
instance.construct(element);
return instance;
diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js
index 3e2deccc86adf..bbc2724387f78 100644
--- a/src/test/ReactTestUtils.js
+++ b/src/test/ReactTestUtils.js
@@ -17,6 +17,7 @@ var EventPropagators = require('EventPropagators');
var React = require('React');
var ReactElement = require('ReactElement');
var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
+var ReactInstanceMap = require('ReactInstanceMap');
var ReactMount = require('ReactMount');
var ReactTextComponent = require('ReactTextComponent');
var ReactUpdates = require('ReactUpdates');
@@ -60,7 +61,9 @@ var ReactTestUtils = {
},
isDOMComponent: function(inst) {
- return !!(inst && inst.mountComponent && inst.tagName);
+ // TODO: Fix this heuristic. It's just here because composites can currently
+ // pretend to be DOM components.
+ return !!(inst && inst.getDOMNode && inst.tagName);
},
isDOMComponentElement: function(inst) {
@@ -101,6 +104,22 @@ var ReactTestUtils = {
return inst instanceof ReactTextComponent.type;
},
+ getInternalRepresentation: function(inst) {
+ // TODO: Remove this duck check once we have a separate DOM/Native instance
+ if (typeof inst.mountComponent === 'function') {
+ return inst;
+ }
+ return ReactInstanceMap.get(inst);
+ },
+
+ getRenderedChildOfCompositeComponent: function(inst) {
+ if (!ReactTestUtils.isCompositeComponent(inst)) {
+ return null;
+ }
+ var internalInstance = ReactInstanceMap.get(inst);
+ return internalInstance._renderedComponent.getPublicInstance();
+ },
+
findAllInRenderedTree: function(inst, test) {
if (!inst) {
return [];
@@ -114,12 +133,18 @@ var ReactTestUtils = {
continue;
}
ret = ret.concat(
- ReactTestUtils.findAllInRenderedTree(renderedChildren[key], test)
+ ReactTestUtils.findAllInRenderedTree(
+ renderedChildren[key].getPublicInstance(),
+ test
+ )
);
}
} else if (ReactTestUtils.isCompositeComponent(inst)) {
ret = ret.concat(
- ReactTestUtils.findAllInRenderedTree(inst._renderedComponent, test)
+ ReactTestUtils.findAllInRenderedTree(
+ ReactTestUtils.getRenderedChildOfCompositeComponent(inst),
+ test
+ )
);
}
return ret;
diff --git a/src/test/reactComponentExpect.js b/src/test/reactComponentExpect.js
index cb03a48b3a058..17c1c7e1afe0c 100644
--- a/src/test/reactComponentExpect.js
+++ b/src/test/reactComponentExpect.js
@@ -12,6 +12,7 @@
"use strict";
+var ReactInstanceMap = require('ReactInstanceMap');
var ReactTestUtils = require('ReactTestUtils');
var assign = require('Object.assign');
@@ -25,7 +26,10 @@ function reactComponentExpect(instance) {
return new reactComponentExpect(instance);
}
- this._instance = instance;
+ expect(instance).not.toBeNull();
+
+ this._instance = ReactTestUtils.getInternalRepresentation(instance);
+
expect(typeof instance).toBe('object');
expect(typeof instance.constructor).toBe('function');
expect(ReactTestUtils.isElement(instance)).toBe(false);
@@ -38,7 +42,7 @@ assign(reactComponentExpect.prototype, {
* @instance: Retrieves the backing instance.
*/
instance: function() {
- return this._instance;
+ return this._instance.getPublicInstance();
},
/**
@@ -57,7 +61,8 @@ assign(reactComponentExpect.prototype, {
*/
expectRenderedChild: function() {
this.toBeCompositeComponent();
- return new reactComponentExpect(this.instance()._renderedComponent);
+ var child = this._instance._renderedComponent;
+ return new reactComponentExpect(child);
},
/**
@@ -67,7 +72,7 @@ assign(reactComponentExpect.prototype, {
// Currently only dom components have arrays of children, but that will
// change soon.
this.toBeDOMComponent();
- var renderedChildren = this.instance()._renderedChildren || {};
+ var renderedChildren = this._instance._renderedChildren || {};
for (var name in renderedChildren) {
if (!renderedChildren.hasOwnProperty(name)) {
continue;
@@ -83,15 +88,15 @@ assign(reactComponentExpect.prototype, {
toBeDOMComponentWithChildCount: function(n) {
this.toBeDOMComponent();
- expect(this.instance()._renderedChildren).toBeTruthy();
- var len = Object.keys(this.instance()._renderedChildren).length;
+ expect(this._instance._renderedChildren).toBeTruthy();
+ var len = Object.keys(this._instance._renderedChildren).length;
expect(len).toBe(n);
return this;
},
toBeDOMComponentWithNoChildren: function() {
this.toBeDOMComponent();
- expect(this.instance()._renderedChildren).toBeFalsy();
+ expect(this._instance._renderedChildren).toBeFalsy();
return this;
},
@@ -99,7 +104,7 @@ assign(reactComponentExpect.prototype, {
toBeComponentOfType: function(constructor) {
expect(
- this.instance()._currentElement.type === constructor
+ this._instance._currentElement.type === constructor
).toBe(true);
return this;
},
@@ -110,6 +115,7 @@ assign(reactComponentExpect.prototype, {
*/
toBeCompositeComponent: function() {
expect(
+ typeof this.instance() === 'object' &&
typeof this.instance().render === 'function' &&
typeof this.instance().setState === 'function'
).toBe(true);
@@ -119,7 +125,7 @@ assign(reactComponentExpect.prototype, {
toBeCompositeComponentWithType: function(constructor) {
this.toBeCompositeComponent();
expect(
- this.instance()._currentElement.type === constructor
+ this._instance._currentElement.type === constructor
).toBe(true);
return this;
},