mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Batch together calls to setState, setProps, etc
The end of ReactUpdates-test.js is probably most illuminating for seeing how this works.
This commit is contained in:
+58
-11
@@ -26,6 +26,7 @@ var ReactID = require('ReactID');
|
||||
var ReactMount = require('ReactMount');
|
||||
var ReactOwner = require('ReactOwner');
|
||||
var ReactReconcileTransaction = require('ReactReconcileTransaction');
|
||||
var ReactUpdates = require('ReactUpdates');
|
||||
|
||||
var invariant = require('invariant');
|
||||
var keyMirror = require('keyMirror');
|
||||
@@ -266,7 +267,11 @@ var ReactComponent = {
|
||||
* @public
|
||||
*/
|
||||
setProps: function(partialProps, callback) {
|
||||
this.replaceProps(merge(this.props, partialProps), callback);
|
||||
// Merge with `_pendingProps` if it exists, otherwise with existing props.
|
||||
this.replaceProps(
|
||||
merge(this._pendingProps || this.props, partialProps),
|
||||
callback
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -286,11 +291,8 @@ var ReactComponent = {
|
||||
'`render` method to pass the correct value as props to the component ' +
|
||||
'where it is created.'
|
||||
);
|
||||
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(this.receiveProps, this, props, transaction);
|
||||
ReactComponent.ReactReconcileTransaction.release(transaction);
|
||||
|
||||
callback && callback();
|
||||
this._pendingProps = props;
|
||||
ReactUpdates.enqueueUpdate(this, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -310,6 +312,9 @@ var ReactComponent = {
|
||||
// All components start unmounted.
|
||||
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
|
||||
|
||||
this._pendingProps = null;
|
||||
this._pendingCallbacks = null;
|
||||
|
||||
// Children can be more than one argument
|
||||
var childrenLength = arguments.length - 1;
|
||||
if (childrenLength === 1) {
|
||||
@@ -396,17 +401,59 @@ var ReactComponent = {
|
||||
this.isMounted(),
|
||||
'receiveProps(...): Can only update a mounted component.'
|
||||
);
|
||||
this._pendingProps = nextProps;
|
||||
this._performUpdateIfNecessary(transaction);
|
||||
},
|
||||
|
||||
/**
|
||||
* Call `_performUpdateIfNecessary` within a new transaction.
|
||||
*
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @internal
|
||||
*/
|
||||
performUpdateIfNecessary: function() {
|
||||
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(this._performUpdateIfNecessary, this, transaction);
|
||||
ReactComponent.ReactReconcileTransaction.release(transaction);
|
||||
},
|
||||
|
||||
/**
|
||||
* If `_pendingProps` is set, update the component.
|
||||
*
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @internal
|
||||
*/
|
||||
_performUpdateIfNecessary: function(transaction) {
|
||||
if (this._pendingProps == null) {
|
||||
return;
|
||||
}
|
||||
var prevProps = this.props;
|
||||
this.props = this._pendingProps;
|
||||
this._pendingProps = null;
|
||||
this.updateComponent(transaction, prevProps);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the component's currently mounted representation.
|
||||
*
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @param {object} prevProps
|
||||
* @internal
|
||||
*/
|
||||
updateComponent: function(transaction, prevProps) {
|
||||
var props = this.props;
|
||||
// If either the owner or a `ref` has changed, make sure the newest owner
|
||||
// has stored a reference to `this`, and the previous owner (if different)
|
||||
// has forgotten the reference to `this`.
|
||||
if (nextProps[OWNER] !== props[OWNER] || nextProps.ref !== props.ref) {
|
||||
if (props.ref != null) {
|
||||
ReactOwner.removeComponentAsRefFrom(this, props.ref, props[OWNER]);
|
||||
if (props[OWNER] !== prevProps[OWNER] || props.ref !== prevProps.ref) {
|
||||
if (prevProps.ref != null) {
|
||||
ReactOwner.removeComponentAsRefFrom(
|
||||
this, prevProps.ref, prevProps[OWNER]
|
||||
);
|
||||
}
|
||||
// Correct, even if the owner is the same, and only the ref has changed.
|
||||
if (nextProps.ref != null) {
|
||||
ReactOwner.addComponentAsRefTo(this, nextProps.ref, nextProps[OWNER]);
|
||||
if (props.ref != null) {
|
||||
ReactOwner.addComponentAsRefTo(this, props.ref, props[OWNER]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -22,6 +22,7 @@ var ReactComponent = require('ReactComponent');
|
||||
var ReactCurrentOwner = require('ReactCurrentOwner');
|
||||
var ReactOwner = require('ReactOwner');
|
||||
var ReactPropTransferer = require('ReactPropTransferer');
|
||||
var ReactUpdates = require('ReactUpdates');
|
||||
|
||||
var invariant = require('invariant');
|
||||
var keyMirror = require('keyMirror');
|
||||
@@ -507,6 +508,7 @@ var ReactCompositeComponentMixin = {
|
||||
|
||||
this.state = this.getInitialState ? this.getInitialState() : null;
|
||||
this._pendingState = null;
|
||||
this._pendingForceUpdate = false;
|
||||
|
||||
if (this.componentWillMount) {
|
||||
this.componentWillMount();
|
||||
@@ -558,31 +560,6 @@ var ReactCompositeComponentMixin = {
|
||||
// TODO: this.state = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the rendered DOM nodes given a new set of props.
|
||||
*
|
||||
* @param {object} nextProps Next set of properties.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @final
|
||||
* @internal
|
||||
*/
|
||||
receiveProps: function(nextProps, transaction) {
|
||||
this._processProps(nextProps);
|
||||
ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
|
||||
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
|
||||
if (this.componentWillReceiveProps) {
|
||||
this.componentWillReceiveProps(nextProps, transaction);
|
||||
}
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
|
||||
// When receiving props, calls to `setState` by `componentWillReceiveProps`
|
||||
// will set `this._pendingState` without triggering a re-render.
|
||||
var nextState = this._pendingState || this.state;
|
||||
this._pendingState = null;
|
||||
this._receivePropsAndState(nextProps, nextState, transaction);
|
||||
this._compositeLifeCycleState = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets a subset of the state. Always use this or `replaceState` to mutate
|
||||
* state. You should treat `this.state` as immutable.
|
||||
@@ -602,7 +579,10 @@ var ReactCompositeComponentMixin = {
|
||||
*/
|
||||
setState: function(partialState, callback) {
|
||||
// Merge with `_pendingState` if it exists, otherwise with existing state.
|
||||
this.replaceState(merge(this._pendingState || this.state, partialState), callback);
|
||||
this.replaceState(
|
||||
merge(this._pendingState || this.state, partialState),
|
||||
callback
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -618,33 +598,9 @@ var ReactCompositeComponentMixin = {
|
||||
* @protected
|
||||
*/
|
||||
replaceState: function(completeState, callback) {
|
||||
var compositeLifeCycleState = this._compositeLifeCycleState;
|
||||
validateLifeCycleOnReplaceState.call(null, this);
|
||||
this._pendingState = completeState;
|
||||
|
||||
// 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) {
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
|
||||
|
||||
var nextState = this._pendingState;
|
||||
this._pendingState = null;
|
||||
|
||||
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(
|
||||
this._receivePropsAndState,
|
||||
this,
|
||||
this.props,
|
||||
nextState,
|
||||
transaction
|
||||
);
|
||||
ReactComponent.ReactReconcileTransaction.release(transaction);
|
||||
this._compositeLifeCycleState = null;
|
||||
}
|
||||
|
||||
// If callback is 'truthy', execute it
|
||||
callback && callback();
|
||||
ReactUpdates.enqueueUpdate(this, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -674,18 +630,52 @@ var ReactCompositeComponentMixin = {
|
||||
}
|
||||
},
|
||||
|
||||
performUpdateIfNecessary: function() {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
ReactComponent.Mixin.performUpdateIfNecessary.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Receives next props and next state, and negotiates whether or not the
|
||||
* component should update as a result.
|
||||
* If any of `_pendingProps`, `_pendingState`, or `_pendingForceUpdate` is
|
||||
* set, update the component.
|
||||
*
|
||||
* @param {object} nextProps Next object to set as props.
|
||||
* @param {?object} nextState Next object to set as state.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @private
|
||||
* @internal
|
||||
*/
|
||||
_receivePropsAndState: function(nextProps, nextState, transaction) {
|
||||
if (!this.shouldComponentUpdate ||
|
||||
_performUpdateIfNecessary: function(transaction) {
|
||||
if (this._pendingProps == null &&
|
||||
this._pendingState == null &&
|
||||
!this._pendingForceUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
var nextProps = this.props;
|
||||
if (this._pendingProps != null) {
|
||||
nextProps = this._pendingProps;
|
||||
this._processProps(nextProps);
|
||||
this._pendingProps = null;
|
||||
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
|
||||
if (this.componentWillReceiveProps) {
|
||||
this.componentWillReceiveProps(nextProps, transaction);
|
||||
}
|
||||
}
|
||||
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
|
||||
|
||||
var nextState = this._pendingState || this.state;
|
||||
this._pendingState = null;
|
||||
|
||||
if (this._pendingForceUpdate ||
|
||||
!this.shouldComponentUpdate ||
|
||||
this.shouldComponentUpdate(nextProps, nextState)) {
|
||||
this._pendingForceUpdate = false;
|
||||
// Will set `this.props` and `this.state`.
|
||||
this._performComponentUpdate(nextProps, nextState, transaction);
|
||||
} else {
|
||||
@@ -694,6 +684,8 @@ var ReactCompositeComponentMixin = {
|
||||
this.props = nextProps;
|
||||
this.state = nextState;
|
||||
}
|
||||
|
||||
this._compositeLifeCycleState = null;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -716,7 +708,7 @@ var ReactCompositeComponentMixin = {
|
||||
this.props = nextProps;
|
||||
this.state = nextState;
|
||||
|
||||
this.updateComponent(transaction);
|
||||
this.updateComponent(transaction, prevProps, prevState);
|
||||
|
||||
if (this.componentDidUpdate) {
|
||||
transaction.getReactOnDOMReady().enqueue(
|
||||
@@ -733,10 +725,13 @@ var ReactCompositeComponentMixin = {
|
||||
* Sophisticated clients may wish to override this.
|
||||
*
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @param {object} prevProps
|
||||
* @param {?object} prevState
|
||||
* @internal
|
||||
* @overridable
|
||||
*/
|
||||
updateComponent: function(transaction) {
|
||||
updateComponent: function(transaction, prevProps, prevState) {
|
||||
ReactComponent.Mixin.updateComponent.call(this, transaction, prevProps);
|
||||
var currentComponent = this._renderedComponent;
|
||||
var nextComponent = this._renderValidatedComponent();
|
||||
if (currentComponent.constructor === nextComponent.constructor) {
|
||||
@@ -783,17 +778,8 @@ var ReactCompositeComponentMixin = {
|
||||
'forceUpdate(...): Cannot force an update while unmounting component ' +
|
||||
'or during an existing state transition (such as within `render`).'
|
||||
);
|
||||
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(
|
||||
this._performComponentUpdate,
|
||||
this,
|
||||
this.props,
|
||||
this.state,
|
||||
transaction
|
||||
);
|
||||
ReactComponent.ReactReconcileTransaction.release(transaction);
|
||||
|
||||
callback && callback();
|
||||
this._pendingForceUpdate = true;
|
||||
ReactUpdates.enqueueUpdate(this, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,6 +23,7 @@ var EventConstants = require('EventConstants');
|
||||
var EventListener = require('EventListener');
|
||||
var EventPluginHub = require('EventPluginHub');
|
||||
var ExecutionEnvironment = require('ExecutionEnvironment');
|
||||
var ReactUpdates = require('ReactUpdates');
|
||||
var ViewportMetrics = require('ViewportMetrics');
|
||||
|
||||
var invariant = require('invariant');
|
||||
@@ -320,8 +321,10 @@ var ReactEventEmitter = {
|
||||
);
|
||||
|
||||
// Event queue being processed in the same cycle allows `preventDefault`.
|
||||
EventPluginHub.enqueueEvents(events);
|
||||
EventPluginHub.processEventQueue();
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
EventPluginHub.enqueueEvents(events);
|
||||
EventPluginHub.processEventQueue();
|
||||
});
|
||||
},
|
||||
|
||||
registrationNames: EventPluginHub.registrationNames,
|
||||
|
||||
+14
-5
@@ -105,11 +105,16 @@ var ReactMount = {
|
||||
* @param {ReactComponent} prevComponent component instance already in the DOM
|
||||
* @param {ReactComponent} nextComponent component instance to render
|
||||
* @param {DOMElement} container container to render into
|
||||
* @param {?function} callback function triggered on completion
|
||||
*/
|
||||
_updateRootComponent: function(prevComponent, nextComponent, container) {
|
||||
_updateRootComponent: function(
|
||||
prevComponent,
|
||||
nextComponent,
|
||||
container,
|
||||
callback) {
|
||||
var nextProps = nextComponent.props;
|
||||
ReactMount.scrollMonitor(container, function() {
|
||||
prevComponent.replaceProps(nextProps);
|
||||
prevComponent.replaceProps(nextProps, callback);
|
||||
});
|
||||
return prevComponent;
|
||||
},
|
||||
@@ -157,9 +162,10 @@ var ReactMount = {
|
||||
*
|
||||
* @param {ReactComponent} nextComponent Component instance to render.
|
||||
* @param {DOMElement} container DOM element to render into.
|
||||
* @param {?function} callback function triggered on completion
|
||||
* @return {ReactComponent} Component instance rendered in `container`.
|
||||
*/
|
||||
renderComponent: function(nextComponent, container) {
|
||||
renderComponent: function(nextComponent, container, callback) {
|
||||
var registeredComponent = instanceByReactRootID[getReactRootID(container)];
|
||||
|
||||
if (registeredComponent) {
|
||||
@@ -167,7 +173,8 @@ var ReactMount = {
|
||||
return ReactMount._updateRootComponent(
|
||||
registeredComponent,
|
||||
nextComponent,
|
||||
container
|
||||
container,
|
||||
callback
|
||||
);
|
||||
} else {
|
||||
ReactMount.unmountAndReleaseReactRootNode(container);
|
||||
@@ -181,11 +188,13 @@ var ReactMount = {
|
||||
|
||||
var shouldReuseMarkup = containerHasReactMarkup && !registeredComponent;
|
||||
|
||||
return ReactMount._renderNewRootComponent(
|
||||
var component = ReactMount._renderNewRootComponent(
|
||||
nextComponent,
|
||||
container,
|
||||
shouldReuseMarkup
|
||||
);
|
||||
callback && callback();
|
||||
return component;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -168,20 +168,24 @@ ReactNativeComponent.Mixin = {
|
||||
return '';
|
||||
},
|
||||
|
||||
receiveProps: function(nextProps, transaction) {
|
||||
assertValidProps(nextProps);
|
||||
ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
|
||||
},
|
||||
|
||||
/**
|
||||
* Controls a native DOM component after it has already been allocated and
|
||||
* Updates a native DOM component after it has already been allocated and
|
||||
* attached to the DOM. Reconciles the root DOM node, then recurses.
|
||||
*
|
||||
* @internal
|
||||
* @param {object} nextProps
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @param {object} prevProps
|
||||
* @internal
|
||||
* @overridable
|
||||
*/
|
||||
receiveProps: function(nextProps, transaction) {
|
||||
ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
|
||||
assertValidProps(nextProps);
|
||||
this._updateDOMProperties(nextProps);
|
||||
this._updateDOMChildren(nextProps, transaction);
|
||||
this.props = nextProps;
|
||||
updateComponent: function(transaction, prevProps) {
|
||||
ReactComponent.Mixin.updateComponent.call(this, transaction, prevProps);
|
||||
this._updateDOMProperties(prevProps);
|
||||
this._updateDOMChildren(prevProps, transaction);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -196,10 +200,10 @@ ReactNativeComponent.Mixin = {
|
||||
* TODO: Benchmark areas that can be improved with caching.
|
||||
*
|
||||
* @private
|
||||
* @param {object} nextProps
|
||||
* @param {object} lastProps
|
||||
*/
|
||||
_updateDOMProperties: function(nextProps) {
|
||||
var lastProps = this.props;
|
||||
_updateDOMProperties: function(lastProps) {
|
||||
var nextProps = this.props;
|
||||
var propKey;
|
||||
var styleName;
|
||||
var styleUpdates;
|
||||
@@ -293,20 +297,21 @@ ReactNativeComponent.Mixin = {
|
||||
* Reconciles the children with the various properties that affect the
|
||||
* children content.
|
||||
*
|
||||
* @param {object} nextProps
|
||||
* @param {object} lastProps
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
*/
|
||||
_updateDOMChildren: function(nextProps, transaction) {
|
||||
var lastUsedContent =
|
||||
CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;
|
||||
_updateDOMChildren: function(lastProps, transaction) {
|
||||
var nextProps = this.props;
|
||||
|
||||
var lastUsedContent =
|
||||
CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
|
||||
var contentToUse =
|
||||
CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
|
||||
|
||||
// Note the use of `!=` which checks for null or undefined.
|
||||
|
||||
var lastUsedChildren =
|
||||
lastUsedContent != null ? null : this.props.children;
|
||||
lastUsedContent != null ? null : lastProps.children;
|
||||
var childrenToUse = contentToUse != null ? null : nextProps.children;
|
||||
|
||||
if (contentToUse != null) {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule ReactUpdates
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var invariant = require('invariant');
|
||||
|
||||
var isBatchingUpdates = false;
|
||||
|
||||
var dirtyComponents = [];
|
||||
|
||||
/**
|
||||
* Call the provided function in a context within which calls to `setState` and
|
||||
* friends are batched such that components aren't updated unnecessarily.
|
||||
*/
|
||||
function batchedUpdates(callback) {
|
||||
invariant(
|
||||
!isBatchingUpdates,
|
||||
'batchedUpates(...): Attempt to batch updates in a context where updates ' +
|
||||
'are already being batched.'
|
||||
);
|
||||
isBatchingUpdates = true;
|
||||
|
||||
callback();
|
||||
// TODO: Sort components by depth such that parent components update first
|
||||
for (var i = 0; i < dirtyComponents.length; i++) {
|
||||
// If a component is unmounted before pending changes apply, ignore them
|
||||
// TODO: Queue unmounts in the same list to avoid this happening at all
|
||||
var component = dirtyComponents[i];
|
||||
if (component.isMounted()) {
|
||||
// If performUpdateIfNecessary happens to enqueue any new updates, we
|
||||
// shouldn't execute the callbacks until the next render happens, so
|
||||
// stash the callbacks first
|
||||
var callbacks = component._pendingCallbacks;
|
||||
component._pendingCallbacks = null;
|
||||
component.performUpdateIfNecessary();
|
||||
if (callbacks) {
|
||||
for (var j = 0; j < callbacks.length; j++) {
|
||||
callbacks[j]();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dirtyComponents.length = 0;
|
||||
|
||||
isBatchingUpdates = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.'
|
||||
);
|
||||
|
||||
if (!isBatchingUpdates) {
|
||||
component.performUpdateIfNecessary();
|
||||
callback && callback();
|
||||
return;
|
||||
}
|
||||
|
||||
dirtyComponents.push(component);
|
||||
|
||||
if (callback) {
|
||||
if (component._pendingCallbacks) {
|
||||
component._pendingCallbacks.push(callback);
|
||||
} else {
|
||||
component._pendingCallbacks = [callback];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ReactUpdates = {
|
||||
batchedUpdates: batchedUpdates,
|
||||
enqueueUpdate: enqueueUpdate
|
||||
};
|
||||
|
||||
module.exports = ReactUpdates;
|
||||
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* Copyright 2013 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var React;
|
||||
var ReactTestUtils;
|
||||
var ReactUpdates;
|
||||
|
||||
describe('ReactUpdates', function() {
|
||||
beforeEach(function() {
|
||||
React = require('React');
|
||||
ReactTestUtils = require('ReactTestUtils');
|
||||
ReactUpdates = require('ReactUpdates');
|
||||
});
|
||||
|
||||
it('should batch state when updating state twice', function() {
|
||||
var updateCount = 0;
|
||||
var Component = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {x: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
updateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div>{this.state.x}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Component />);
|
||||
expect(instance.state.x).toBe(0);
|
||||
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
instance.setState({x: 1});
|
||||
instance.setState({x: 2});
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(updateCount).toBe(0);
|
||||
});
|
||||
|
||||
expect(instance.state.x).toBe(2);
|
||||
expect(updateCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should batch state when updating two different state keys', function() {
|
||||
var updateCount = 0;
|
||||
var Component = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {x: 0, y: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
updateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div>({this.state.x}, {this.state.y})</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Component />);
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(instance.state.y).toBe(0);
|
||||
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
instance.setState({x: 1});
|
||||
instance.setState({y: 2});
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(instance.state.y).toBe(0);
|
||||
expect(updateCount).toBe(0);
|
||||
});
|
||||
|
||||
expect(instance.state.x).toBe(1);
|
||||
expect(instance.state.y).toBe(2);
|
||||
expect(updateCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should batch state and props together', function() {
|
||||
var updateCount = 0;
|
||||
var Component = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {y: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
updateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div>({this.props.x}, {this.state.y})</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Component x={0} />);
|
||||
expect(instance.props.x).toBe(0);
|
||||
expect(instance.state.y).toBe(0);
|
||||
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
instance.setProps({x: 1});
|
||||
instance.setState({y: 2});
|
||||
expect(instance.props.x).toBe(0);
|
||||
expect(instance.state.y).toBe(0);
|
||||
expect(updateCount).toBe(0);
|
||||
});
|
||||
|
||||
expect(instance.props.x).toBe(1);
|
||||
expect(instance.state.y).toBe(2);
|
||||
expect(updateCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should batch parent/child state updates together', function() {
|
||||
var parentUpdateCount = 0;
|
||||
var Parent = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {x: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
parentUpdateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div><Child ref="child" x={this.state.x} /></div>;
|
||||
}
|
||||
});
|
||||
var childUpdateCount = 0;
|
||||
var Child = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {y: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
childUpdateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div>{this.props.x + this.state.y}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Parent />);
|
||||
var child = instance.refs.child;
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(child.state.y).toBe(0);
|
||||
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
instance.setState({x: 1});
|
||||
child.setState({y: 2});
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(child.state.y).toBe(0);
|
||||
expect(parentUpdateCount).toBe(0);
|
||||
expect(childUpdateCount).toBe(0);
|
||||
});
|
||||
|
||||
expect(instance.state.x).toBe(1);
|
||||
expect(child.state.y).toBe(2);
|
||||
expect(parentUpdateCount).toBe(1);
|
||||
expect(childUpdateCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should batch child/parent state updates together', function() {
|
||||
var parentUpdateCount = 0;
|
||||
var Parent = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {x: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
parentUpdateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div><Child ref="child" x={this.state.x} /></div>;
|
||||
}
|
||||
});
|
||||
var childUpdateCount = 0;
|
||||
var Child = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {y: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
childUpdateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div>{this.props.x + this.state.y}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Parent />);
|
||||
var child = instance.refs.child;
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(child.state.y).toBe(0);
|
||||
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
child.setState({y: 2});
|
||||
instance.setState({x: 1});
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(child.state.y).toBe(0);
|
||||
expect(parentUpdateCount).toBe(0);
|
||||
expect(childUpdateCount).toBe(0);
|
||||
});
|
||||
|
||||
expect(instance.state.x).toBe(1);
|
||||
expect(child.state.y).toBe(2);
|
||||
expect(parentUpdateCount).toBe(1);
|
||||
|
||||
// When we update the child first, we currently incur two updates because
|
||||
// we aren't smart about what order to process the components in.
|
||||
// TODO: Reduce the update count here to 1
|
||||
expect(childUpdateCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should support chained state updates', function() {
|
||||
var updateCount = 0;
|
||||
var Component = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {x: 0};
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
updateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div>{this.state.x}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Component />);
|
||||
expect(instance.state.x).toBe(0);
|
||||
|
||||
var innerCallbackRun = false;
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
instance.setState({x: 1}, function() {
|
||||
instance.setState({x: 2}, function() {
|
||||
innerCallbackRun = true;
|
||||
expect(instance.state.x).toBe(2);
|
||||
expect(updateCount).toBe(2);
|
||||
});
|
||||
expect(instance.state.x).toBe(1);
|
||||
expect(updateCount).toBe(1);
|
||||
});
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(updateCount).toBe(0);
|
||||
});
|
||||
|
||||
expect(innerCallbackRun).toBeTruthy();
|
||||
expect(instance.state.x).toBe(2);
|
||||
expect(updateCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should batch forceUpdate together', function() {
|
||||
var shouldUpdateCount = 0;
|
||||
var updateCount = 0;
|
||||
var Component = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {x: 0};
|
||||
},
|
||||
shouldComponentUpdate: function() {
|
||||
shouldUpdateCount++;
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
updateCount++;
|
||||
},
|
||||
render: function() {
|
||||
return <div>{this.state.x}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = ReactTestUtils.renderIntoDocument(<Component />);
|
||||
expect(instance.state.x).toBe(0);
|
||||
|
||||
var callbacksRun = 0;
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
instance.setState({x: 1}, function() {
|
||||
callbacksRun++;
|
||||
});
|
||||
instance.forceUpdate(function() {
|
||||
callbacksRun++;
|
||||
});
|
||||
expect(instance.state.x).toBe(0);
|
||||
expect(updateCount).toBe(0);
|
||||
});
|
||||
|
||||
expect(callbacksRun).toBe(2);
|
||||
// shouldComponentUpdate shouldn't be called since we're forcing
|
||||
expect(shouldUpdateCount).toBe(0);
|
||||
expect(instance.state.x).toBe(1);
|
||||
expect(updateCount).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -46,7 +46,9 @@ var ClickCounter = React.createClass({
|
||||
}
|
||||
return (
|
||||
<span className="clickIncrementer"
|
||||
onClick={this.setState.bind(this, {count: this.state.count + 1})}>
|
||||
onClick={function() {
|
||||
this.setState({count: this.state.count + 1});
|
||||
}.bind(this)}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user