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
);