Add ReactUpdates.setImmediate for async callbacks
Callbacks passed to this setImmediate function are called at the end of the current update cycle, which is guaranteed to be asynchronous but in the same event loop (with the default batching strategy). This is useful for new-style refs (#1373, #1554) and for fixing #1698. Test Plan: jest
Ben Alpert committed
Jun 28, 2014 at 16:46 UTC
12b532c253e6efc3d077b35f9c3aa88ed94fc435
2 files changed
+109
-7
src/core/ReactUpdates.js
+34
-7
@@ -29,6 +29,8 @@ var mixInto = require('mixInto');
29
var warning = require('warning');
30
31
var dirtyComponents = [];
32
+var setImmediateCallbackQueue = CallbackQueue.getPooled();
33
+var setImmediateEnqueued = false;
34
35
var batchingStrategy = null;
36
@@ -73,7 +75,7 @@ var TRANSACTION_WRAPPERS = [NESTED_UPDATES, UPDATE_QUEUEING];
75
function ReactUpdatesFlushTransaction() {
76
this.reinitializeTransaction();
77
this.dirtyComponentsLength = null;
76
- this.callbackQueue = CallbackQueue.getPooled(null);
78
+ this.callbackQueue = CallbackQueue.getPooled();
79
this.reconcileTransaction =
80
ReactUpdates.ReactReconcileTransaction.getPooled();
81
}
@@ -170,11 +172,21 @@ var flushBatchedUpdates = ReactPerf.measure(
172
// ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
173
// array and perform any updates enqueued by mount-ready handlers (i.e.,
174
// componentDidUpdate) but we need to check here too in order to catch
173
- // updates enqueued by setState callbacks.
174
- while (dirtyComponents.length) {
175
- var transaction = ReactUpdatesFlushTransaction.getPooled();
176
- transaction.perform(runBatchedUpdates, null, transaction);
177
- ReactUpdatesFlushTransaction.release(transaction);
175
+ // updates enqueued by setState callbacks and setImmediate calls.
176
+ while (dirtyComponents.length || setImmediateEnqueued) {
177
+ if (dirtyComponents.length) {
178
+ var transaction = ReactUpdatesFlushTransaction.getPooled();
179
+ transaction.perform(runBatchedUpdates, null, transaction);
180
+ ReactUpdatesFlushTransaction.release(transaction);
181
+ }
182
+
183
+ if (setImmediateEnqueued) {
184
+ setImmediateEnqueued = false;
185
+ var queue = setImmediateCallbackQueue;
186
+ setImmediateCallbackQueue = CallbackQueue.getPooled();
187
+ queue.notifyAll();
188
+ CallbackQueue.release(queue);
189
+ }
190
}
191
}
192
);
@@ -221,6 +233,20 @@ function enqueueUpdate(component, callback) {
233
}
234
}
235
236
+/**
237
+ * Enqueue a callback to be run at the end of the current batching cycle. Throws
238
+ * if no updates are currently being performed.
239
+ */
240
+function setImmediate(callback, context) {
241
+ invariant(
242
+ batchingStrategy.isBatchingUpdates,
243
+ 'ReactUpdates.setImmediate: Can\'t enqueue an immediate callback in a ' +
244
+ 'context where updates are not being batched.'
245
+ );
246
+ setImmediateCallbackQueue.enqueue(callback, context);
247
+ setImmediateEnqueued = true;
248
+}
249
+
250
var ReactUpdatesInjection = {
251
injectReconcileTransaction: function(ReconcileTransaction) {
252
invariant(
@@ -259,7 +285,8 @@ var ReactUpdates = {
285
batchedUpdates: batchedUpdates,
286
enqueueUpdate: enqueueUpdate,
287
flushBatchedUpdates: flushBatchedUpdates,
262
- injection: ReactUpdatesInjection
288
+ injection: ReactUpdatesInjection,
289
+ setImmediate: setImmediate
290
};
291
292
module.exports = ReactUpdates;
src/core/__tests__/ReactUpdates-test.js
+75
@@ -748,4 +748,79 @@ describe('ReactUpdates', function() {
748
React.renderComponent(<A x={2} />, container);
749
expect(callbackCount).toBe(1);
750
});
751
+
752
+ it('calls setImmediate callbacks properly', function() {
753
+ var callbackCount = 0;
754
+ var A = React.createClass({
755
+ render: function() {
756
+ return <div />;
757
+ },
758
+ componentDidUpdate: function() {
759
+ var component = this;
760
+ ReactUpdates.setImmediate(function() {
761
+ expect(this).toBe(component);
762
+ callbackCount++;
763
+ ReactUpdates.setImmediate(function() {
764
+ callbackCount++;
765
+ });
766
+ expect(callbackCount).toBe(1);
767
+ }, this);
768
+ expect(callbackCount).toBe(0);
769
+ }
770
+ });
771
+
772
+ var container = document.createElement('div');
773
+ var component = React.renderComponent(<A />, container);
774
+ component.forceUpdate();
775
+ expect(callbackCount).toBe(2);
776
+ });
777
+
778
+ it('calls setImmediate callbacks with queued updates', function() {
779
+ var log = [];
780
+ var A = React.createClass({
781
+ getInitialState: () => ({updates: 0}),
782
+ render: function() {
783
+ log.push('render-' + this.state.updates);
784
+ return <div />;
785
+ },
786
+ componentDidUpdate: function() {
787
+ if (this.state.updates === 1) {
788
+ ReactUpdates.setImmediate(function() {
789
+ this.setState({updates: 2}, function() {
790
+ ReactUpdates.setImmediate(function() {
791
+ log.push('setImmediate-1.2');
792
+ });
793
+ log.push('setState-cb');
794
+ });
795
+ log.push('setImmediate-1.1');
796
+ }, this);
797
+ } else if (this.state.updates === 2) {
798
+ ReactUpdates.setImmediate(function() {
799
+ log.push('setImmediate-2');
800
+ });
801
+ }
802
+ log.push('didUpdate-' + this.state.updates);
803
+ }
804
+ });
805
+
806
+ var container = document.createElement('div');
807
+ var component = React.renderComponent(<A />, container);
808
+ component.setState({updates: 1});
809
+ expect(log).toEqual([
810
+ 'render-0',
811
+ // We do the first update...
812
+ 'render-1',
813
+ 'didUpdate-1',
814
+ // ...which calls a setImmediate and enqueues a second update...
815
+ 'setImmediate-1.1',
816
+ // ...which runs and enqueues the setImmediate-2 log in its didUpdate...
817
+ 'render-2',
818
+ 'didUpdate-2',
819
+ // ...and runs the setState callback, which enqueues the log for
820
+ // setImmediate-1.2.
821
+ 'setState-cb',
822
+ 'setImmediate-2',
823
+ 'setImmediate-1.2'
824
+ ]);
825
+ });
826
});