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; },