From ed7332c74921874cdcb3cce629bb5bd8c95a7968 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 27 Jan 2015 00:55:25 -0800 Subject: [PATCH] Extract setState, setProps etc into ReactUpdateQueue I originally did this work so that we could allow setState to be called before the internal ReactCompositeComponent was constructed. It's unlikely that we'll go down that route now but this still seems like a better abstraction. It communicates that this is not immediately updating an object oriented class. It's just a queue which a minor optimization. It also avoids bloating the ReactCompositeComponent file. Since they both depend on the life cycle I break that out into a common shared dependency. In a follow up I'll refactor the life-cycle management. --- src/browser/ui/ReactMount.js | 7 +- src/classic/class/ReactClass.js | 37 +-- src/core/ReactCompositeComponent.js | 211 +------------- src/core/ReactLifeCycle.js | 60 ++++ src/core/ReactUpdateQueue.js | 264 ++++++++++++++++++ src/core/ReactUpdates.js | 18 +- .../__tests__/ReactComponentLifeCycle-test.js | 12 +- .../__tests__/ReactCompositeComponent-test.js | 16 +- .../ReactCompositeComponentState-test.js | 2 +- src/modern/class/ReactComponentBase.js | 29 +- 10 files changed, 377 insertions(+), 279 deletions(-) create mode 100644 src/core/ReactLifeCycle.js create mode 100644 src/core/ReactUpdateQueue.js diff --git a/src/browser/ui/ReactMount.js b/src/browser/ui/ReactMount.js index a71b2311d4..5b030c447e 100644 --- a/src/browser/ui/ReactMount.js +++ b/src/browser/ui/ReactMount.js @@ -22,6 +22,7 @@ var ReactInstanceMap = require('ReactInstanceMap'); var ReactMarkupChecksum = require('ReactMarkupChecksum'); var ReactPerf = require('ReactPerf'); var ReactReconciler = require('ReactReconciler'); +var ReactUpdateQueue = require('ReactUpdateQueue'); var ReactUpdates = require('ReactUpdates'); var emptyObject = require('emptyObject'); @@ -299,9 +300,11 @@ var ReactMount = { ReactElementValidator.checkAndWarnForMutatedProps(nextElement); } - var nextProps = nextElement.props; ReactMount.scrollMonitor(container, function() { - prevComponent.replaceProps(nextProps, callback); + ReactUpdateQueue.enqueueElementInternal(prevComponent, nextElement); + if (callback) { + ReactUpdateQueue.enqueueCallbackInternal(prevComponent, callback); + } }); if (__DEV__) { diff --git a/src/classic/class/ReactClass.js b/src/classic/class/ReactClass.js index b5359d076b..5b9e5647bf 100644 --- a/src/classic/class/ReactClass.js +++ b/src/classic/class/ReactClass.js @@ -17,6 +17,7 @@ var ReactErrorUtils = require('ReactErrorUtils'); var ReactInstanceMap = require('ReactInstanceMap'); var ReactPropTypeLocations = require('ReactPropTypeLocations'); var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames'); +var ReactUpdateQueue = require('ReactUpdateQueue'); var assign = require('Object.assign'); var invariant = require('invariant'); @@ -719,16 +720,10 @@ var ReactClassMixin = { * 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. ' + - 'This usually means you called replaceState() on an unmounted component.' - ); - internalInstance.replaceState( - newState, - callback - ); + ReactUpdateQueue.enqueueReplaceState(this, newState); + if (callback) { + ReactUpdateQueue.enqueueCallback(this, callback); + } }, /** @@ -754,16 +749,10 @@ var ReactClassMixin = { * @deprecated */ setProps: function(partialProps, callback) { - var internalInstance = ReactInstanceMap.get(this); - invariant( - internalInstance, - 'setProps(...): Can only update a mounted or mounting component. ' + - 'This usually means you called setProps() on an unmounted component.' - ); - internalInstance.setProps( - partialProps, - callback - ); + ReactUpdateQueue.enqueueSetProps(this, partialProps); + if (callback) { + ReactUpdateQueue.enqueueCallback(this, callback); + } }, /** @@ -776,10 +765,10 @@ var ReactClassMixin = { * @deprecated */ replaceProps: function(newProps, callback) { - ReactInstanceMap.get(this).replaceProps( - newProps, - callback - ); + ReactUpdateQueue.enqueueReplaceProps(this, newProps); + if (callback) { + ReactUpdateQueue.enqueueCallback(this, callback); + } } }; diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index 830705a5ed..8d5481a7c5 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -17,6 +17,7 @@ var ReactCurrentOwner = require('ReactCurrentOwner'); var ReactElement = require('ReactElement'); var ReactElementValidator = require('ReactElementValidator'); var ReactInstanceMap = require('ReactInstanceMap'); +var ReactLifeCycle = require('ReactLifeCycle'); var ReactNativeComponent = require('ReactNativeComponent'); var ReactPerf = require('ReactPerf'); var ReactPropTypeLocations = require('ReactPropTypeLocations'); @@ -27,8 +28,6 @@ var ReactUpdates = require('ReactUpdates'); var assign = require('Object.assign'); var emptyObject = require('emptyObject'); var invariant = require('invariant'); -var keyMirror = require('keyMirror'); -var monitorCodeUse = require('monitorCodeUse'); var shouldUpdateReactComponent = require('shouldUpdateReactComponent'); var warning = require('warning'); @@ -43,50 +42,6 @@ function getDeclarationErrorAddendum(component) { return ''; } -/** - * `ReactCompositeComponent` maintains an auxiliary life cycle state in - * `this._compositeLifeCycleState` (which can be null). - * - * This is different from the life cycle state maintained by `ReactComponent`. - * The following diagram shows how the states overlap in - * time. There are times when the CompositeLifeCycle is null - at those times it - * is only meaningful to look at ComponentLifeCycle alone. - * - * Top Row: ReactComponent.ComponentLifeCycle - * Low Row: ReactComponent.CompositeLifeCycle - * - * +-------+---------------------------------+--------+ - * | UN | MOUNTED | UN | - * |MOUNTED| | MOUNTED| - * +-------+---------------------------------+--------+ - * | ^--------+ +-------+ +--------^ | - * | | | | | | | | - * | 0--|MOUNTING|-0-|RECEIVE|-0-| UN |--->0 | - * | | | |PROPS | |MOUNTING| | - * | | | | | | | | - * | | | | | | | | - * | +--------+ +-------+ +--------+ | - * | | | | - * +-------+---------------------------------+--------+ - */ -var CompositeLifeCycle = keyMirror({ - /** - * Components in the process of being mounted respond to state changes - * differently. - */ - MOUNTING: null, - /** - * Components in the process of being unmounted are guarded against state - * changes. - */ - UNMOUNTING: null, - /** - * Components that are mounted and receiving new props respond to state - * changes differently. - */ - RECEIVING_PROPS: null -}); - /** * An incrementing ID assigned to each component when it is mounted. This is * used to enforce the order in which `ReactUpdates` updates dirty components. @@ -112,6 +67,7 @@ var ReactCompositeComponentMixin = { this._rootNodeID = null; this._instance = null; + // See ReactUpdateQueue this._pendingElement = null; this._pendingState = null; this._pendingForceUpdate = false; @@ -123,7 +79,7 @@ var ReactCompositeComponentMixin = { this._mountOrder = 0; this._isTopLevel = false; - // See ReactUpdates. + // See ReactUpdates and ReactUpdateQueue. this._pendingCallbacks = null; }, @@ -134,7 +90,7 @@ var ReactCompositeComponentMixin = { * @final */ isMounted: function() { - return this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING; + return this._compositeLifeCycleState !== ReactLifeCycle.MOUNTING; }, /** @@ -171,7 +127,7 @@ var ReactCompositeComponentMixin = { // Store a reference from the instance back to the internal representation ReactInstanceMap.set(inst, this); - this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING; + this._compositeLifeCycleState = ReactLifeCycle.MOUNTING; if (__DEV__) { this._warnIfContextsDiffer(this._currentElement._context, context); @@ -265,7 +221,7 @@ var ReactCompositeComponentMixin = { unmountComponent: function() { var inst = this._instance; - this._compositeLifeCycleState = CompositeLifeCycle.UNMOUNTING; + this._compositeLifeCycleState = ReactLifeCycle.UNMOUNTING; if (inst.componentWillUnmount) { inst.componentWillUnmount(); } @@ -297,50 +253,6 @@ var ReactCompositeComponentMixin = { // TODO: inst.context = null; }, - /** - * Sets a subset of the props. - * - * @param {object} partialProps Subset of the next props. - * @param {?function} callback Called after props are updated. - * @final - * @public - */ - setProps: function(partialProps, callback) { - // Merge with the pending element if it exists, otherwise with existing - // element props. - var element = this._pendingElement || this._currentElement; - this.replaceProps( - assign({}, element.props, partialProps), - callback - ); - }, - - /** - * Replaces all of the props. - * - * @param {object} props New props. - * @param {?function} callback Called after props are updated. - * @final - * @public - */ - replaceProps: function(props, callback) { - invariant( - this._isTopLevel, - 'replaceProps(...): You called `setProps` or `replaceProps` on a ' + - 'component with a parent. This is an anti-pattern since props will ' + - 'get reactively updated when rendered. Instead, change the owner\'s ' + - '`render` method to pass the correct value as props to the component ' + - 'where it is created.' - ); - // This is a deoptimized path. We optimize for always having an element. - // This creates an extra internal element. - this._pendingElement = ReactElement.cloneAndReplaceProps( - this._pendingElement || this._currentElement, - props - ); - ReactUpdates.enqueueUpdate(this, callback); - }, - /** * Schedule a partial update to the props. Only used for internal testing. * @@ -360,109 +272,6 @@ var ReactCompositeComponentMixin = { ReactUpdates.enqueueUpdate(this, callback); }, - /** - * 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. - * @final - * @protected - */ - setState: function(partialState, callback) { - var compositeLifeCycleState = this._compositeLifeCycleState; - invariant( - ReactCurrentOwner.current == null, - 'setState(...): Cannot update during an existing state transition ' + - '(such as within `render`). Render methods should be a pure function ' + - 'of props and state.' - ); - invariant( - compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING, - 'setState(...): Cannot call setState() on an unmounting component.' - ); - // Merge with `_pendingState` if it exists, otherwise with existing state. - this._pendingState = assign( - {}, - this._pendingState || this._instance.state, - partialState - ); - if (this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING) { - // If we're in a componentWillMount handler, don't enqueue a rerender - // because ReactUpdates assumes we're in a browser context (which is wrong - // for server rendering) and we're about to do a render anyway. - // TODO: The callback here is ignored when setState is called from - // componentWillMount. Either fix it or disallow doing so completely in - // favor of getInitialState. - ReactUpdates.enqueueUpdate(this, callback); - } - }, - - /** - * Replaces all of the state. Always use this or `setState` 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. - * - * @param {object} completeState Next state. - * @param {?function} callback Called after state is updated. - * @final - * @protected - */ - replaceState: function(completeState, callback) { - var compositeLifeCycleState = this._compositeLifeCycleState; - invariant( - ReactCurrentOwner.current == null, - 'replaceState(...): Cannot update during an existing state transition ' + - '(such as within `render`). Render methods should be a pure function ' + - 'of props and state.' - ); - invariant( - compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING, - 'replaceState(...): Cannot call replaceState() on an unmounting ' + - 'component.' - ); - this._pendingState = completeState; - if (this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING) { - // If we're in a componentWillMount handler, don't enqueue a rerender - // because ReactUpdates assumes we're in a browser context (which is wrong - // for server rendering) and we're about to do a render anyway. - // TODO: The callback here is ignored when setState is called from - // componentWillMount. Either fix it or disallow doing so completely in - // favor of getInitialState. - ReactUpdates.enqueueUpdate(this, callback); - } - }, - - /** - * 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` @@ -626,8 +435,8 @@ var ReactCompositeComponentMixin = { var compositeLifeCycleState = this._compositeLifeCycleState; // Do not trigger a state transition if we are in the middle of mounting or // receiving props because both of those will already be doing this. - if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING || - compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) { + if (compositeLifeCycleState === ReactLifeCycle.MOUNTING || + compositeLifeCycleState === ReactLifeCycle.RECEIVING_PROPS) { return; } @@ -743,7 +552,7 @@ var ReactCompositeComponentMixin = { } } - this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS; + this._compositeLifeCycleState = ReactLifeCycle.RECEIVING_PROPS; if (inst.componentWillReceiveProps) { inst.componentWillReceiveProps(nextProps, nextContext); } @@ -1003,8 +812,6 @@ ReactPerf.measureMethods( var ReactCompositeComponent = { - LifeCycle: CompositeLifeCycle, - Mixin: ReactCompositeComponentMixin }; diff --git a/src/core/ReactLifeCycle.js b/src/core/ReactLifeCycle.js new file mode 100644 index 0000000000..81ae5c9bbd --- /dev/null +++ b/src/core/ReactLifeCycle.js @@ -0,0 +1,60 @@ +/** + * Copyright 2015, 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 ReactLifeCycle + */ + +"use strict"; + +var keyMirror = require('keyMirror'); + +/** + * `ReactCompositeComponent` maintains an auxiliary life cycle state in + * `this._compositeLifeCycleState` (which can be null). + * + * This is different from the life cycle state maintained by `ReactComponent`. + * The following diagram shows how the states overlap in + * time. There are times when the CompositeLifeCycle is null - at those times it + * is only meaningful to look at ComponentLifeCycle alone. + * + * Top Row: ReactComponent.ComponentLifeCycle + * Low Row: ReactComponent.CompositeLifeCycle + * + * +-------+---------------------------------+--------+ + * | UN | MOUNTED | UN | + * |MOUNTED| | MOUNTED| + * +-------+---------------------------------+--------+ + * | ^--------+ +-------+ +--------^ | + * | | | | | | | | + * | 0--|MOUNTING|-0-|RECEIVE|-0-| UN |--->0 | + * | | | |PROPS | |MOUNTING| | + * | | | | | | | | + * | | | | | | | | + * | +--------+ +-------+ +--------+ | + * | | | | + * +-------+---------------------------------+--------+ + */ +var ReactLifeCycle = keyMirror({ + /** + * Components in the process of being mounted respond to state changes + * differently. + */ + MOUNTING: null, + /** + * Components in the process of being unmounted are guarded against state + * changes. + */ + UNMOUNTING: null, + /** + * Components that are mounted and receiving new props respond to state + * changes differently. + */ + RECEIVING_PROPS: null +}); + +module.exports = ReactLifeCycle; diff --git a/src/core/ReactUpdateQueue.js b/src/core/ReactUpdateQueue.js new file mode 100644 index 0000000000..88fe04b55d --- /dev/null +++ b/src/core/ReactUpdateQueue.js @@ -0,0 +1,264 @@ +/** + * Copyright 2015, 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 ReactUpdateQueue + */ + +"use strict"; + +var ReactLifeCycle = require('ReactLifeCycle'); +var ReactCurrentOwner = require('ReactCurrentOwner'); +var ReactElement = require('ReactElement'); +var ReactInstanceMap = require('ReactInstanceMap'); +var ReactUpdates = require('ReactUpdates'); + +var assign = require('Object.assign'); +var invariant = require('invariant'); + +function enqueueUpdate(internalInstance) { + if (internalInstance._compositeLifeCycleState !== ReactLifeCycle.MOUNTING) { + // If we're in a componentWillMount handler, don't enqueue a rerender + // because ReactUpdates assumes we're in a browser context (which is + // wrong for server rendering) and we're about to do a render anyway. + // See bug in #1740. + ReactUpdates.enqueueUpdate(internalInstance); + } +} + +function getInternalInstanceReadyForUpdate(publicInstance, callerName) { + invariant( + ReactCurrentOwner.current == null, + '%s(...): Cannot update during an existing state transition ' + + '(such as within `render`). Render methods should be a pure function ' + + 'of props and state.', + callerName + ); + + var internalInstance = ReactInstanceMap.get(publicInstance); + invariant( + internalInstance, + '%s(...): Can only update a mounted or mounting component. ' + + 'This usually means you called %s() on an unmounted ' + + 'component.', + callerName, + callerName + ); + invariant( + internalInstance._compositeLifeCycleState !== + ReactLifeCycle.UNMOUNTING, + '%s(...): Cannot call %s() on an unmounting component.', + callerName, + callerName + ); + return internalInstance; +} + +/** + * ReactUpdateQueue allows for state updates to be scheduled into a later + * reconciliation step. + */ +var ReactUpdateQueue = { + + /** + * Enqueue a callback that will be executed after all the pending updates + * have processed. + * + * @param {ReactClass} publicInstance The instance to use as `this` context. + * @param {?function} callback Called after state is updated. + * @internal + */ + enqueueCallback: function(publicInstance, callback) { + invariant( + typeof callback === 'function', + 'enqueueCallback(...): You called `setProps`, `replaceProps`, ' + + '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + + 'isn\'t callable.' + ); + var internalInstance = ReactInstanceMap.get(publicInstance); + invariant( + internalInstance, + 'Cannot enqueue a callback on an instance that is unmounted.' + ); + if (internalInstance._compositeLifeCycleState === ReactLifeCycle.MOUNTING) { + // Ignore callbacks in componentWillMount. See enqueueUpdate. + return; + } + if (internalInstance._pendingCallbacks) { + internalInstance._pendingCallbacks.push(callback); + } else { + internalInstance._pendingCallbacks = [callback]; + } + // TODO: The callback here is ignored when setState is called from + // componentWillMount. Either fix it or disallow doing so completely in + // favor of getInitialState. + enqueueUpdate(internalInstance); + }, + + enqueueCallbackInternal: function(internalInstance, callback) { + invariant( + typeof callback === "function", + 'enqueueCallback(...): You called `setProps`, `replaceProps`, ' + + '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + + 'isn\'t callable.' + ); + if (internalInstance._pendingCallbacks) { + internalInstance._pendingCallbacks.push(callback); + } else { + internalInstance._pendingCallbacks = [callback]; + } + }, + + /** + * 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 {ReactClass} publicInstance The instance that should rerender. + * @internal + */ + enqueueForceUpdate: function(publicInstance) { + var internalInstance = getInternalInstanceReadyForUpdate( + publicInstance, + 'forceUpdate' + ); + + internalInstance._pendingForceUpdate = true; + + enqueueUpdate(internalInstance); + }, + + /** + * Replaces all of the state. Always use this or `setState` 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. + * + * @param {ReactClass} publicInstance The instance that should rerender. + * @param {object} completeState Next state. + * @internal + */ + enqueueReplaceState: function(publicInstance, completeState) { + var internalInstance = getInternalInstanceReadyForUpdate( + publicInstance, + 'replaceState' + ); + + internalInstance._pendingState = completeState; + + enqueueUpdate(internalInstance); + }, + + /** + * 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 {ReactClass} publicInstance The instance that should rerender. + * @param {object} partialState Next partial state to be merged with state. + * @internal + */ + enqueueSetState: function(publicInstance, partialState) { + var internalInstance = getInternalInstanceReadyForUpdate( + publicInstance, + 'setState' + ); + + // Merge with `_pendingState` if it exists, otherwise with existing state. + internalInstance._pendingState = assign( + {}, + internalInstance._pendingState || internalInstance._instance.state, + partialState + ); + + enqueueUpdate(internalInstance); + }, + + /** + * Sets a subset of the props. + * + * @param {ReactClass} publicInstance The instance that should rerender. + * @param {object} partialProps Subset of the next props. + * @internal + */ + enqueueSetProps: function(publicInstance, partialProps) { + var internalInstance = getInternalInstanceReadyForUpdate( + publicInstance, + 'setProps' + ); + + invariant( + internalInstance._isTopLevel, + 'setProps(...): You called `setProps` on a ' + + 'component with a parent. This is an anti-pattern since props will ' + + 'get reactively updated when rendered. Instead, change the owner\'s ' + + '`render` method to pass the correct value as props to the component ' + + 'where it is created.' + ); + + // Merge with the pending element if it exists, otherwise with existing + // element props. + var element = internalInstance._pendingElement || + internalInstance._currentElement; + var props = assign({}, element.props, partialProps); + internalInstance._pendingElement = ReactElement.cloneAndReplaceProps( + element, + props + ); + + enqueueUpdate(internalInstance); + }, + + /** + * Replaces all of the props. + * + * @param {ReactClass} publicInstance The instance that should rerender. + * @param {object} props New props. + * @internal + */ + enqueueReplaceProps: function(publicInstance, props) { + var internalInstance = getInternalInstanceReadyForUpdate( + publicInstance, + 'replaceProps' + ); + + invariant( + internalInstance._isTopLevel, + 'replaceProps(...): You called `replaceProps` on a ' + + 'component with a parent. This is an anti-pattern since props will ' + + 'get reactively updated when rendered. Instead, change the owner\'s ' + + '`render` method to pass the correct value as props to the component ' + + 'where it is created.' + ); + + // Merge with the pending element if it exists, otherwise with existing + // element props. + var element = internalInstance._pendingElement || + internalInstance._currentElement; + internalInstance._pendingElement = ReactElement.cloneAndReplaceProps( + element, + props + ); + + enqueueUpdate(internalInstance); + }, + + enqueueElementInternal: function(internalInstance, newElement) { + internalInstance._pendingElement = newElement; + enqueueUpdate(internalInstance); + } + +}; + +module.exports = ReactUpdateQueue; diff --git a/src/core/ReactUpdates.js b/src/core/ReactUpdates.js index 7323715d82..dc84254f06 100644 --- a/src/core/ReactUpdates.js +++ b/src/core/ReactUpdates.js @@ -190,13 +190,7 @@ flushBatchedUpdates = ReactPerf.measure( * Mark a component as needing a rerender, adding an optional callback to a * list of functions which will be executed once the rerender occurs. */ -function enqueueUpdate(component, callback) { - invariant( - !callback || typeof callback === 'function', - 'enqueueUpdate(...): You called `setProps`, `replaceProps`, ' + - '`setState`, `replaceState`, or `forceUpdate` with a callback that ' + - 'isn\'t callable.' - ); +function enqueueUpdate(component) { ensureInjected(); // Various parts of our code (such as ReactCompositeComponent's @@ -213,19 +207,11 @@ function enqueueUpdate(component, callback) { ); if (!batchingStrategy.isBatchingUpdates) { - batchingStrategy.batchedUpdates(enqueueUpdate, component, callback); + batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } dirtyComponents.push(component); - - if (callback) { - if (component._pendingCallbacks) { - component._pendingCallbacks.push(callback); - } else { - component._pendingCallbacks = [callback]; - } - } } /** diff --git a/src/core/__tests__/ReactComponentLifeCycle-test.js b/src/core/__tests__/ReactComponentLifeCycle-test.js index 936313905c..ab3223ed1e 100644 --- a/src/core/__tests__/ReactComponentLifeCycle-test.js +++ b/src/core/__tests__/ReactComponentLifeCycle-test.js @@ -97,7 +97,7 @@ describe('ReactComponentLifeCycle', function() { React = require('React'); ReactTestUtils = require('ReactTestUtils'); ReactCompositeComponent = require('ReactCompositeComponent'); - CompositeComponentLifeCycle = ReactCompositeComponent.LifeCycle; + CompositeComponentLifeCycle = require('ReactLifeCycle'); ReactInstanceMap = require('ReactInstanceMap'); @@ -403,11 +403,11 @@ describe('ReactComponentLifeCycle', function() { expect(function() { instance = ReactTestUtils.renderIntoDocument(instance); }).toThrow( - 'Invariant Violation: replaceProps(...): You called `setProps` or ' + - '`replaceProps` on a component with a parent. This is an anti-pattern ' + - 'since props will get reactively updated when rendered. Instead, ' + - 'change the owner\'s `render` method to pass the correct value as ' + - 'props to the component where it is created.' + 'Invariant Violation: setProps(...): You called `setProps` on a ' + + 'component with a parent. This is an anti-pattern since props will get ' + + 'reactively updated when rendered. Instead, change the owner\'s ' + + '`render` method to pass the correct value as props to the component ' + + 'where it is created.' ); }); diff --git a/src/core/__tests__/ReactCompositeComponent-test.js b/src/core/__tests__/ReactCompositeComponent-test.js index ee705d4e2f..b51a2db21f 100644 --- a/src/core/__tests__/ReactCompositeComponent-test.js +++ b/src/core/__tests__/ReactCompositeComponent-test.js @@ -294,9 +294,9 @@ describe('ReactCompositeComponent', function() { expect(function() { instance.forceUpdate(); }).toThrow( - 'Invariant Violation: forceUpdate(...): Can only force an update on ' + - 'mounted or mounting components. This usually means you called ' + - 'forceUpdate() on an unmounted component.' + 'Invariant Violation: forceUpdate(...): Can only update a mounted or ' + + 'mounting component. This usually means you called forceUpdate() on ' + + 'an unmounted component.' ); }); @@ -406,11 +406,11 @@ describe('ReactCompositeComponent', function() { expect(function() { innerInstance.setProps({value: 1}); }).toThrow( - 'Invariant Violation: replaceProps(...): You called `setProps` or ' + - '`replaceProps` on a component with a parent. This is an anti-pattern ' + - 'since props will get reactively updated when rendered. Instead, ' + - 'change the owner\'s `render` method to pass the correct value as ' + - 'props to the component where it is created.' + 'Invariant Violation: setProps(...): You called `setProps` on a ' + + 'component with a parent. This is an anti-pattern since props will get ' + + 'reactively updated when rendered. Instead, change the owner\'s ' + + '`render` method to pass the correct value as props to the component ' + + 'where it is created.' ); }); diff --git a/src/core/__tests__/ReactCompositeComponentState-test.js b/src/core/__tests__/ReactCompositeComponentState-test.js index 95d53d91e3..ffa1ac2e8a 100644 --- a/src/core/__tests__/ReactCompositeComponentState-test.js +++ b/src/core/__tests__/ReactCompositeComponentState-test.js @@ -154,7 +154,7 @@ describe('ReactCompositeComponent-state', function() { [ 'render', 'orange', null ], [ 'componentDidMount-start', 'orange', null ], // setState-sunrise and setState-orange should be called here, - // after the bug in # + // after the bug in #1740 // componentDidMount() called setState({color:'yellow'}), currently this // occurs inline. // In a future where setState() is async, this test result will change. diff --git a/src/modern/class/ReactComponentBase.js b/src/modern/class/ReactComponentBase.js index fcdc0e2b79..c09ba53ea9 100644 --- a/src/modern/class/ReactComponentBase.js +++ b/src/modern/class/ReactComponentBase.js @@ -11,7 +11,7 @@ 'use strict'; -var ReactInstanceMap = require('ReactInstanceMap'); +var ReactUpdateQueue = require('ReactUpdateQueue'); var invariant = require('invariant'); var warning = require('warning'); @@ -53,17 +53,10 @@ ReactComponentBase.prototype.setState = function(partialState, callback) { 'instead, use forceUpdate().' ); } - - var internalInstance = ReactInstanceMap.get(this); - invariant( - internalInstance, - 'setState(...): Can only update a mounted or mounting component. ' + - 'This usually means you called setState() on an unmounted ' + - 'component.' - ); - internalInstance.setState( - partialState, callback - ); + ReactUpdateQueue.enqueueSetState(this, partialState); + if (callback) { + ReactUpdateQueue.enqueueCallback(this, callback); + } }; /** @@ -81,14 +74,10 @@ ReactComponentBase.prototype.setState = function(partialState, callback) { * @protected */ ReactComponentBase.prototype.forceUpdate = function(callback) { - var internalInstance = ReactInstanceMap.get(this); - invariant( - internalInstance, - 'forceUpdate(...): Can only force an update on mounted or mounting ' + - 'components. This usually means you called forceUpdate() on an ' + - 'unmounted component.' - ); - internalInstance.forceUpdate(callback); + ReactUpdateQueue.enqueueForceUpdate(this); + if (callback) { + ReactUpdateQueue.enqueueCallback(this, callback); + } }; /**