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:
Ben Alpert
2013-07-02 00:04:50 -07:00
parent 0e9ee239a9
commit d9e99d4688
8 changed files with 552 additions and 107 deletions
+58 -11
View File
@@ -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]);
}
}
},
+57 -71
View File
@@ -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);
},
/**
+5 -2
View File
@@ -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
View File
@@ -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;
},
/**
+22 -17
View File
@@ -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) {
+98
View File
@@ -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;
+295
View File
@@ -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);
});
});
+3 -1
View File
@@ -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>
);