@samitouri / QOS-React-2 / commits / d9e99d4688
Fork 0

The library for web and native user interfaces.

Batch together calls to setState, setProps, etc

The end of ReactUpdates-test.js is probably most illuminating for seeing how this works.

Ben Alpert committed Jun 27, 2013 at 01:24 UTC d9e99d468851a5e0c23c8d205bd953ee67819231
8 files changed +552 -107
src/core/ReactComponent.js
+58 -11
@@ -26,6 +26,7 @@ var ReactID = require('ReactID');
26 var ReactMount = require('ReactMount');
27 var ReactOwner = require('ReactOwner');
28 var ReactReconcileTransaction = require('ReactReconcileTransaction');
29 +var ReactUpdates = require('ReactUpdates');
30
31 var invariant = require('invariant');
32 var keyMirror = require('keyMirror');
@@ -266,7 +267,11 @@ var ReactComponent = {
267 * @public
268 */
269 setProps: function(partialProps, callback) {
269 - this.replaceProps(merge(this.props, partialProps), callback);
270 + // Merge with `_pendingProps` if it exists, otherwise with existing props.
271 + this.replaceProps(
272 + merge(this._pendingProps || this.props, partialProps),
273 + callback
274 + );
275 },
276
277 /**
@@ -286,11 +291,8 @@ var ReactComponent = {
291 '`render` method to pass the correct value as props to the component ' +
292 'where it is created.'
293 );
289 - var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
290 - transaction.perform(this.receiveProps, this, props, transaction);
291 - ReactComponent.ReactReconcileTransaction.release(transaction);
292 -
293 - callback && callback();
294 + this._pendingProps = props;
295 + ReactUpdates.enqueueUpdate(this, callback);
296 },
297
298 /**
@@ -310,6 +312,9 @@ var ReactComponent = {
312 // All components start unmounted.
313 this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
314
315 + this._pendingProps = null;
316 + this._pendingCallbacks = null;
317 +
318 // Children can be more than one argument
319 var childrenLength = arguments.length - 1;
320 if (childrenLength === 1) {
@@ -396,17 +401,59 @@ var ReactComponent = {
401 this.isMounted(),
402 'receiveProps(...): Can only update a mounted component.'
403 );
404 + this._pendingProps = nextProps;
405 + this._performUpdateIfNecessary(transaction);
406 + },
407 +
408 + /**
409 + * Call `_performUpdateIfNecessary` within a new transaction.
410 + *
411 + * @param {ReactReconcileTransaction} transaction
412 + * @internal
413 + */
414 + performUpdateIfNecessary: function() {
415 + var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
416 + transaction.perform(this._performUpdateIfNecessary, this, transaction);
417 + ReactComponent.ReactReconcileTransaction.release(transaction);
418 + },
419 +
420 + /**
421 + * If `_pendingProps` is set, update the component.
422 + *
423 + * @param {ReactReconcileTransaction} transaction
424 + * @internal
425 + */
426 + _performUpdateIfNecessary: function(transaction) {
427 + if (this._pendingProps == null) {
428 + return;
429 + }
430 + var prevProps = this.props;
431 + this.props = this._pendingProps;
432 + this._pendingProps = null;
433 + this.updateComponent(transaction, prevProps);
434 + },
435 +
436 + /**
437 + * Updates the component's currently mounted representation.
438 + *
439 + * @param {ReactReconcileTransaction} transaction
440 + * @param {object} prevProps
441 + * @internal
442 + */
443 + updateComponent: function(transaction, prevProps) {
444 var props = this.props;
445 // If either the owner or a `ref` has changed, make sure the newest owner
446 // has stored a reference to `this`, and the previous owner (if different)
447 // has forgotten the reference to `this`.
403 - if (nextProps[OWNER] !== props[OWNER] || nextProps.ref !== props.ref) {
404 - if (props.ref != null) {
405 - ReactOwner.removeComponentAsRefFrom(this, props.ref, props[OWNER]);
448 + if (props[OWNER] !== prevProps[OWNER] || props.ref !== prevProps.ref) {
449 + if (prevProps.ref != null) {
450 + ReactOwner.removeComponentAsRefFrom(
451 + this, prevProps.ref, prevProps[OWNER]
452 + );
453 }
454 // Correct, even if the owner is the same, and only the ref has changed.
408 - if (nextProps.ref != null) {
409 - ReactOwner.addComponentAsRefTo(this, nextProps.ref, nextProps[OWNER]);
455 + if (props.ref != null) {
456 + ReactOwner.addComponentAsRefTo(this, props.ref, props[OWNER]);
457 }
458 }
459 },
src/core/ReactCompositeComponent.js
+57 -71
@@ -22,6 +22,7 @@ var ReactComponent = require('ReactComponent');
22 var ReactCurrentOwner = require('ReactCurrentOwner');
23 var ReactOwner = require('ReactOwner');
24 var ReactPropTransferer = require('ReactPropTransferer');
25 +var ReactUpdates = require('ReactUpdates');
26
27 var invariant = require('invariant');
28 var keyMirror = require('keyMirror');
@@ -507,6 +508,7 @@ var ReactCompositeComponentMixin = {
508
509 this.state = this.getInitialState ? this.getInitialState() : null;
510 this._pendingState = null;
511 + this._pendingForceUpdate = false;
512
513 if (this.componentWillMount) {
514 this.componentWillMount();
@@ -558,31 +560,6 @@ var ReactCompositeComponentMixin = {
560 // TODO: this.state = null;
561 },
562
561 - /**
562 - * Updates the rendered DOM nodes given a new set of props.
563 - *
564 - * @param {object} nextProps Next set of properties.
565 - * @param {ReactReconcileTransaction} transaction
566 - * @final
567 - * @internal
568 - */
569 - receiveProps: function(nextProps, transaction) {
570 - this._processProps(nextProps);
571 - ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
572 -
573 - this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
574 - if (this.componentWillReceiveProps) {
575 - this.componentWillReceiveProps(nextProps, transaction);
576 - }
577 - this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
578 - // When receiving props, calls to `setState` by `componentWillReceiveProps`
579 - // will set `this._pendingState` without triggering a re-render.
580 - var nextState = this._pendingState || this.state;
581 - this._pendingState = null;
582 - this._receivePropsAndState(nextProps, nextState, transaction);
583 - this._compositeLifeCycleState = null;
584 - },
585 -
563 /**
564 * Sets a subset of the state. Always use this or `replaceState` to mutate
565 * state. You should treat `this.state` as immutable.
@@ -602,7 +579,10 @@ var ReactCompositeComponentMixin = {
579 */
580 setState: function(partialState, callback) {
581 // Merge with `_pendingState` if it exists, otherwise with existing state.
605 - this.replaceState(merge(this._pendingState || this.state, partialState), callback);
582 + this.replaceState(
583 + merge(this._pendingState || this.state, partialState),
584 + callback
585 + );
586 },
587
588 /**
@@ -618,33 +598,9 @@ var ReactCompositeComponentMixin = {
598 * @protected
599 */
600 replaceState: function(completeState, callback) {
621 - var compositeLifeCycleState = this._compositeLifeCycleState;
601 validateLifeCycleOnReplaceState.call(null, this);
602 this._pendingState = completeState;
624 -
625 - // Do not trigger a state transition if we are in the middle of mounting or
626 - // receiving props because both of those will already be doing this.
627 - if (compositeLifeCycleState !== CompositeLifeCycle.MOUNTING &&
628 - compositeLifeCycleState !== CompositeLifeCycle.RECEIVING_PROPS) {
629 - this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
630 -
631 - var nextState = this._pendingState;
632 - this._pendingState = null;
633 -
634 - var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
635 - transaction.perform(
636 - this._receivePropsAndState,
637 - this,
638 - this.props,
639 - nextState,
640 - transaction
641 - );
642 - ReactComponent.ReactReconcileTransaction.release(transaction);
643 - this._compositeLifeCycleState = null;
644 - }
645 -
646 - // If callback is 'truthy', execute it
647 - callback && callback();
603 + ReactUpdates.enqueueUpdate(this, callback);
604 },
605
606 /**
@@ -674,18 +630,52 @@ var ReactCompositeComponentMixin = {
630 }
631 },
632
633 + performUpdateIfNecessary: function() {
634 + var compositeLifeCycleState = this._compositeLifeCycleState;
635 + // Do not trigger a state transition if we are in the middle of mounting or
636 + // receiving props because both of those will already be doing this.
637 + if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING ||
638 + compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
639 + return;
640 + }
641 + ReactComponent.Mixin.performUpdateIfNecessary.call(this);
642 + },
643 +
644 /**
678 - * Receives next props and next state, and negotiates whether or not the
679 - * component should update as a result.
645 + * If any of `_pendingProps`, `_pendingState`, or `_pendingForceUpdate` is
646 + * set, update the component.
647 *
681 - * @param {object} nextProps Next object to set as props.
682 - * @param {?object} nextState Next object to set as state.
648 * @param {ReactReconcileTransaction} transaction
684 - * @private
649 + * @internal
650 */
686 - _receivePropsAndState: function(nextProps, nextState, transaction) {
687 - if (!this.shouldComponentUpdate ||
651 + _performUpdateIfNecessary: function(transaction) {
652 + if (this._pendingProps == null &&
653 + this._pendingState == null &&
654 + !this._pendingForceUpdate) {
655 + return;
656 + }
657 +
658 + var nextProps = this.props;
659 + if (this._pendingProps != null) {
660 + nextProps = this._pendingProps;
661 + this._processProps(nextProps);
662 + this._pendingProps = null;
663 +
664 + this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
665 + if (this.componentWillReceiveProps) {
666 + this.componentWillReceiveProps(nextProps, transaction);
667 + }
668 + }
669 +
670 + this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;
671 +
672 + var nextState = this._pendingState || this.state;
673 + this._pendingState = null;
674 +
675 + if (this._pendingForceUpdate ||
676 + !this.shouldComponentUpdate ||
677 this.shouldComponentUpdate(nextProps, nextState)) {
678 + this._pendingForceUpdate = false;
679 // Will set `this.props` and `this.state`.
680 this._performComponentUpdate(nextProps, nextState, transaction);
681 } else {
@@ -694,6 +684,8 @@ var ReactCompositeComponentMixin = {
684 this.props = nextProps;
685 this.state = nextState;
686 }
687 +
688 + this._compositeLifeCycleState = null;
689 },
690
691 /**
@@ -716,7 +708,7 @@ var ReactCompositeComponentMixin = {
708 this.props = nextProps;
709 this.state = nextState;
710
719 - this.updateComponent(transaction);
711 + this.updateComponent(transaction, prevProps, prevState);
712
713 if (this.componentDidUpdate) {
714 transaction.getReactOnDOMReady().enqueue(
@@ -733,10 +725,13 @@ var ReactCompositeComponentMixin = {
725 * Sophisticated clients may wish to override this.
726 *
727 * @param {ReactReconcileTransaction} transaction
728 + * @param {object} prevProps
729 + * @param {?object} prevState
730 * @internal
731 * @overridable
732 */
739 - updateComponent: function(transaction) {
733 + updateComponent: function(transaction, prevProps, prevState) {
734 + ReactComponent.Mixin.updateComponent.call(this, transaction, prevProps);
735 var currentComponent = this._renderedComponent;
736 var nextComponent = this._renderValidatedComponent();
737 if (currentComponent.constructor === nextComponent.constructor) {
@@ -783,17 +778,8 @@ var ReactCompositeComponentMixin = {
778 'forceUpdate(...): Cannot force an update while unmounting component ' +
779 'or during an existing state transition (such as within `render`).'
780 );
786 - var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
787 - transaction.perform(
788 - this._performComponentUpdate,
789 - this,
790 - this.props,
791 - this.state,
792 - transaction
793 - );
794 - ReactComponent.ReactReconcileTransaction.release(transaction);
795 -
796 - callback && callback();
781 + this._pendingForceUpdate = true;
782 + ReactUpdates.enqueueUpdate(this, callback);
783 },
784
785 /**
src/core/ReactEventEmitter.js
+5 -2
@@ -23,6 +23,7 @@ var EventConstants = require('EventConstants');
23 var EventListener = require('EventListener');
24 var EventPluginHub = require('EventPluginHub');
25 var ExecutionEnvironment = require('ExecutionEnvironment');
26 +var ReactUpdates = require('ReactUpdates');
27 var ViewportMetrics = require('ViewportMetrics');
28
29 var invariant = require('invariant');
@@ -320,8 +321,10 @@ var ReactEventEmitter = {
321 );
322
323 // Event queue being processed in the same cycle allows `preventDefault`.
323 - EventPluginHub.enqueueEvents(events);
324 - EventPluginHub.processEventQueue();
324 + ReactUpdates.batchedUpdates(function() {
325 + EventPluginHub.enqueueEvents(events);
326 + EventPluginHub.processEventQueue();
327 + });
328 },
329
330 registrationNames: EventPluginHub.registrationNames,
src/core/ReactMount.js
+14 -5
@@ -105,11 +105,16 @@ var ReactMount = {
105 * @param {ReactComponent} prevComponent component instance already in the DOM
106 * @param {ReactComponent} nextComponent component instance to render
107 * @param {DOMElement} container container to render into
108 + * @param {?function} callback function triggered on completion
109 */
109 - _updateRootComponent: function(prevComponent, nextComponent, container) {
110 + _updateRootComponent: function(
111 + prevComponent,
112 + nextComponent,
113 + container,
114 + callback) {
115 var nextProps = nextComponent.props;
116 ReactMount.scrollMonitor(container, function() {
112 - prevComponent.replaceProps(nextProps);
117 + prevComponent.replaceProps(nextProps, callback);
118 });
119 return prevComponent;
120 },
@@ -157,9 +162,10 @@ var ReactMount = {
162 *
163 * @param {ReactComponent} nextComponent Component instance to render.
164 * @param {DOMElement} container DOM element to render into.
165 + * @param {?function} callback function triggered on completion
166 * @return {ReactComponent} Component instance rendered in `container`.
167 */
162 - renderComponent: function(nextComponent, container) {
168 + renderComponent: function(nextComponent, container, callback) {
169 var registeredComponent = instanceByReactRootID[getReactRootID(container)];
170
171 if (registeredComponent) {
@@ -167,7 +173,8 @@ var ReactMount = {
173 return ReactMount._updateRootComponent(
174 registeredComponent,
175 nextComponent,
170 - container
176 + container,
177 + callback
178 );
179 } else {
180 ReactMount.unmountAndReleaseReactRootNode(container);
@@ -181,11 +188,13 @@ var ReactMount = {
188
189 var shouldReuseMarkup = containerHasReactMarkup && !registeredComponent;
190
184 - return ReactMount._renderNewRootComponent(
191 + var component = ReactMount._renderNewRootComponent(
192 nextComponent,
193 container,
194 shouldReuseMarkup
195 );
196 + callback && callback();
197 + return component;
198 },
199
200 /**
src/core/ReactNativeComponent.js
+22 -17
@@ -168,20 +168,24 @@ ReactNativeComponent.Mixin = {
168 return '';
169 },
170
171 + receiveProps: function(nextProps, transaction) {
172 + assertValidProps(nextProps);
173 + ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
174 + },
175 +
176 /**
172 - * Controls a native DOM component after it has already been allocated and
177 + * Updates a native DOM component after it has already been allocated and
178 * attached to the DOM. Reconciles the root DOM node, then recurses.
179 *
175 - * @internal
176 - * @param {object} nextProps
180 * @param {ReactReconcileTransaction} transaction
181 + * @param {object} prevProps
182 + * @internal
183 + * @overridable
184 */
179 - receiveProps: function(nextProps, transaction) {
180 - ReactComponent.Mixin.receiveProps.call(this, nextProps, transaction);
181 - assertValidProps(nextProps);
182 - this._updateDOMProperties(nextProps);
183 - this._updateDOMChildren(nextProps, transaction);
184 - this.props = nextProps;
185 + updateComponent: function(transaction, prevProps) {
186 + ReactComponent.Mixin.updateComponent.call(this, transaction, prevProps);
187 + this._updateDOMProperties(prevProps);
188 + this._updateDOMChildren(prevProps, transaction);
189 },
190
191 /**
@@ -196,10 +200,10 @@ ReactNativeComponent.Mixin = {
200 * TODO: Benchmark areas that can be improved with caching.
201 *
202 * @private
199 - * @param {object} nextProps
203 + * @param {object} lastProps
204 */
201 - _updateDOMProperties: function(nextProps) {
202 - var lastProps = this.props;
205 + _updateDOMProperties: function(lastProps) {
206 + var nextProps = this.props;
207 var propKey;
208 var styleName;
209 var styleUpdates;
@@ -293,20 +297,21 @@ ReactNativeComponent.Mixin = {
297 * Reconciles the children with the various properties that affect the
298 * children content.
299 *
296 - * @param {object} nextProps
300 + * @param {object} lastProps
301 * @param {ReactReconcileTransaction} transaction
302 */
299 - _updateDOMChildren: function(nextProps, transaction) {
300 - var lastUsedContent =
301 - CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;
303 + _updateDOMChildren: function(lastProps, transaction) {
304 + var nextProps = this.props;
305
306 + var lastUsedContent =
307 + CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
308 var contentToUse =
309 CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
310
311 // Note the use of `!=` which checks for null or undefined.
312
313 var lastUsedChildren =
309 - lastUsedContent != null ? null : this.props.children;
314 + lastUsedContent != null ? null : lastProps.children;
315 var childrenToUse = contentToUse != null ? null : nextProps.children;
316
317 if (contentToUse != null) {
src/core/ReactUpdates.js new
+98
@@ -0,0 +1,98 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @providesModule ReactUpdates
17 + */
18 +
19 +"use strict";
20 +
21 +var invariant = require('invariant');
22 +
23 +var isBatchingUpdates = false;
24 +
25 +var dirtyComponents = [];
26 +
27 +/**
28 + * Call the provided function in a context within which calls to `setState` and
29 + * friends are batched such that components aren't updated unnecessarily.
30 + */
31 +function batchedUpdates(callback) {
32 + invariant(
33 + !isBatchingUpdates,
34 + 'batchedUpates(...): Attempt to batch updates in a context where updates ' +
35 + 'are already being batched.'
36 + );
37 + isBatchingUpdates = true;
38 +
39 + callback();
40 + // TODO: Sort components by depth such that parent components update first
41 + for (var i = 0; i < dirtyComponents.length; i++) {
42 + // If a component is unmounted before pending changes apply, ignore them
43 + // TODO: Queue unmounts in the same list to avoid this happening at all
44 + var component = dirtyComponents[i];
45 + if (component.isMounted()) {
46 + // If performUpdateIfNecessary happens to enqueue any new updates, we
47 + // shouldn't execute the callbacks until the next render happens, so
48 + // stash the callbacks first
49 + var callbacks = component._pendingCallbacks;
50 + component._pendingCallbacks = null;
51 + component.performUpdateIfNecessary();
52 + if (callbacks) {
53 + for (var j = 0; j < callbacks.length; j++) {
54 + callbacks[j]();
55 + }
56 + }
57 + }
58 + }
59 + dirtyComponents.length = 0;
60 +
61 + isBatchingUpdates = false;
62 +}
63 +
64 +/**
65 + * Mark a component as needing a rerender, adding an optional callback to a
66 + * list of functions which will be executed once the rerender occurs.
67 + */
68 +function enqueueUpdate(component, callback) {
69 + invariant(
70 + !callback || typeof callback === "function",
71 + 'enqueueUpdate(...): You called `setProps`, `replaceProps`, ' +
72 + '`setState`, `replaceState`, or `forceUpdate` with a callback that ' +
73 + 'isn\'t callable.'
74 + );
75 +
76 + if (!isBatchingUpdates) {
77 + component.performUpdateIfNecessary();
78 + callback && callback();
79 + return;
80 + }
81 +
82 + dirtyComponents.push(component);
83 +
84 + if (callback) {
85 + if (component._pendingCallbacks) {
86 + component._pendingCallbacks.push(callback);
87 + } else {
88 + component._pendingCallbacks = [callback];
89 + }
90 + }
91 +}
92 +
93 +var ReactUpdates = {
94 + batchedUpdates: batchedUpdates,
95 + enqueueUpdate: enqueueUpdate
96 +};
97 +
98 +module.exports = ReactUpdates;
src/core/__tests__/ReactUpdates-test.js new
+295
@@ -0,0 +1,295 @@
1 +/**
2 + * Copyright 2013 Facebook, Inc.
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + *
16 + * @jsx React.DOM
17 + * @emails react-core
18 + */
19 +
20 +"use strict";
21 +
22 +var React;
23 +var ReactTestUtils;
24 +var ReactUpdates;
25 +
26 +describe('ReactUpdates', function() {
27 + beforeEach(function() {
28 + React = require('React');
29 + ReactTestUtils = require('ReactTestUtils');
30 + ReactUpdates = require('ReactUpdates');
31 + });
32 +
33 + it('should batch state when updating state twice', function() {
34 + var updateCount = 0;
35 + var Component = React.createClass({
36 + getInitialState: function() {
37 + return {x: 0};
38 + },
39 + componentDidUpdate: function() {
40 + updateCount++;
41 + },
42 + render: function() {
43 + return <div>{this.state.x}</div>;
44 + }
45 + });
46 +
47 + var instance = ReactTestUtils.renderIntoDocument(<Component />);
48 + expect(instance.state.x).toBe(0);
49 +
50 + ReactUpdates.batchedUpdates(function() {
51 + instance.setState({x: 1});
52 + instance.setState({x: 2});
53 + expect(instance.state.x).toBe(0);
54 + expect(updateCount).toBe(0);
55 + });
56 +
57 + expect(instance.state.x).toBe(2);
58 + expect(updateCount).toBe(1);
59 + });
60 +
61 + it('should batch state when updating two different state keys', function() {
62 + var updateCount = 0;
63 + var Component = React.createClass({
64 + getInitialState: function() {
65 + return {x: 0, y: 0};
66 + },
67 + componentDidUpdate: function() {
68 + updateCount++;
69 + },
70 + render: function() {
71 + return <div>({this.state.x}, {this.state.y})</div>;
72 + }
73 + });
74 +
75 + var instance = ReactTestUtils.renderIntoDocument(<Component />);
76 + expect(instance.state.x).toBe(0);
77 + expect(instance.state.y).toBe(0);
78 +
79 + ReactUpdates.batchedUpdates(function() {
80 + instance.setState({x: 1});
81 + instance.setState({y: 2});
82 + expect(instance.state.x).toBe(0);
83 + expect(instance.state.y).toBe(0);
84 + expect(updateCount).toBe(0);
85 + });
86 +
87 + expect(instance.state.x).toBe(1);
88 + expect(instance.state.y).toBe(2);
89 + expect(updateCount).toBe(1);
90 + });
91 +
92 + it('should batch state and props together', function() {
93 + var updateCount = 0;
94 + var Component = React.createClass({
95 + getInitialState: function() {
96 + return {y: 0};
97 + },
98 + componentDidUpdate: function() {
99 + updateCount++;
100 + },
101 + render: function() {
102 + return <div>({this.props.x}, {this.state.y})</div>;
103 + }
104 + });
105 +
106 + var instance = ReactTestUtils.renderIntoDocument(<Component x={0} />);
107 + expect(instance.props.x).toBe(0);
108 + expect(instance.state.y).toBe(0);
109 +
110 + ReactUpdates.batchedUpdates(function() {
111 + instance.setProps({x: 1});
112 + instance.setState({y: 2});
113 + expect(instance.props.x).toBe(0);
114 + expect(instance.state.y).toBe(0);
115 + expect(updateCount).toBe(0);
116 + });
117 +
118 + expect(instance.props.x).toBe(1);
119 + expect(instance.state.y).toBe(2);
120 + expect(updateCount).toBe(1);
121 + });
122 +
123 + it('should batch parent/child state updates together', function() {
124 + var parentUpdateCount = 0;
125 + var Parent = React.createClass({
126 + getInitialState: function() {
127 + return {x: 0};
128 + },
129 + componentDidUpdate: function() {
130 + parentUpdateCount++;
131 + },
132 + render: function() {
133 + return <div><Child ref="child" x={this.state.x} /></div>;
134 + }
135 + });
136 + var childUpdateCount = 0;
137 + var Child = React.createClass({
138 + getInitialState: function() {
139 + return {y: 0};
140 + },
141 + componentDidUpdate: function() {
142 + childUpdateCount++;
143 + },
144 + render: function() {
145 + return <div>{this.props.x + this.state.y}</div>;
146 + }
147 + });
148 +
149 + var instance = ReactTestUtils.renderIntoDocument(<Parent />);
150 + var child = instance.refs.child;
151 + expect(instance.state.x).toBe(0);
152 + expect(child.state.y).toBe(0);
153 +
154 + ReactUpdates.batchedUpdates(function() {
155 + instance.setState({x: 1});
156 + child.setState({y: 2});
157 + expect(instance.state.x).toBe(0);
158 + expect(child.state.y).toBe(0);
159 + expect(parentUpdateCount).toBe(0);
160 + expect(childUpdateCount).toBe(0);
161 + });
162 +
163 + expect(instance.state.x).toBe(1);
164 + expect(child.state.y).toBe(2);
165 + expect(parentUpdateCount).toBe(1);
166 + expect(childUpdateCount).toBe(1);
167 + });
168 +
169 + it('should batch child/parent state updates together', function() {
170 + var parentUpdateCount = 0;
171 + var Parent = React.createClass({
172 + getInitialState: function() {
173 + return {x: 0};
174 + },
175 + componentDidUpdate: function() {
176 + parentUpdateCount++;
177 + },
178 + render: function() {
179 + return <div><Child ref="child" x={this.state.x} /></div>;
180 + }
181 + });
182 + var childUpdateCount = 0;
183 + var Child = React.createClass({
184 + getInitialState: function() {
185 + return {y: 0};
186 + },
187 + componentDidUpdate: function() {
188 + childUpdateCount++;
189 + },
190 + render: function() {
191 + return <div>{this.props.x + this.state.y}</div>;
192 + }
193 + });
194 +
195 + var instance = ReactTestUtils.renderIntoDocument(<Parent />);
196 + var child = instance.refs.child;
197 + expect(instance.state.x).toBe(0);
198 + expect(child.state.y).toBe(0);
199 +
200 + ReactUpdates.batchedUpdates(function() {
201 + child.setState({y: 2});
202 + instance.setState({x: 1});
203 + expect(instance.state.x).toBe(0);
204 + expect(child.state.y).toBe(0);
205 + expect(parentUpdateCount).toBe(0);
206 + expect(childUpdateCount).toBe(0);
207 + });
208 +
209 + expect(instance.state.x).toBe(1);
210 + expect(child.state.y).toBe(2);
211 + expect(parentUpdateCount).toBe(1);
212 +
213 + // When we update the child first, we currently incur two updates because
214 + // we aren't smart about what order to process the components in.
215 + // TODO: Reduce the update count here to 1
216 + expect(childUpdateCount).toBe(2);
217 + });
218 +
219 + it('should support chained state updates', function() {
220 + var updateCount = 0;
221 + var Component = React.createClass({
222 + getInitialState: function() {
223 + return {x: 0};
224 + },
225 + componentDidUpdate: function() {
226 + updateCount++;
227 + },
228 + render: function() {
229 + return <div>{this.state.x}</div>;
230 + }
231 + });
232 +
233 + var instance = ReactTestUtils.renderIntoDocument(<Component />);
234 + expect(instance.state.x).toBe(0);
235 +
236 + var innerCallbackRun = false;
237 + ReactUpdates.batchedUpdates(function() {
238 + instance.setState({x: 1}, function() {
239 + instance.setState({x: 2}, function() {
240 + innerCallbackRun = true;
241 + expect(instance.state.x).toBe(2);
242 + expect(updateCount).toBe(2);
243 + });
244 + expect(instance.state.x).toBe(1);
245 + expect(updateCount).toBe(1);
246 + });
247 + expect(instance.state.x).toBe(0);
248 + expect(updateCount).toBe(0);
249 + });
250 +
251 + expect(innerCallbackRun).toBeTruthy();
252 + expect(instance.state.x).toBe(2);
253 + expect(updateCount).toBe(2);
254 + });
255 +
256 + it('should batch forceUpdate together', function() {
257 + var shouldUpdateCount = 0;
258 + var updateCount = 0;
259 + var Component = React.createClass({
260 + getInitialState: function() {
261 + return {x: 0};
262 + },
263 + shouldComponentUpdate: function() {
264 + shouldUpdateCount++;
265 + },
266 + componentDidUpdate: function() {
267 + updateCount++;
268 + },
269 + render: function() {
270 + return <div>{this.state.x}</div>;
271 + }
272 + });
273 +
274 + var instance = ReactTestUtils.renderIntoDocument(<Component />);
275 + expect(instance.state.x).toBe(0);
276 +
277 + var callbacksRun = 0;
278 + ReactUpdates.batchedUpdates(function() {
279 + instance.setState({x: 1}, function() {
280 + callbacksRun++;
281 + });
282 + instance.forceUpdate(function() {
283 + callbacksRun++;
284 + });
285 + expect(instance.state.x).toBe(0);
286 + expect(updateCount).toBe(0);
287 + });
288 +
289 + expect(callbacksRun).toBe(2);
290 + // shouldComponentUpdate shouldn't be called since we're forcing
291 + expect(shouldUpdateCount).toBe(0);
292 + expect(instance.state.x).toBe(1);
293 + expect(updateCount).toBe(1);
294 + });
295 +});
src/core/__tests__/refs-test.js
+3 -1
@@ -46,7 +46,9 @@ var ClickCounter = React.createClass({
46 }
47 return (
48 <span className="clickIncrementer"
49 - onClick={this.setState.bind(this, {count: this.state.count + 1})}>
49 + onClick={function() {
50 + this.setState({count: this.state.count + 1});
51 + }.bind(this)}>
52 {children}
53 </span>
54 );