Skip to content

Commit

Permalink
Allow injection of a batching function to wrap dispatches
Browse files Browse the repository at this point in the history
React automatically batches updates when inside a synthetic event
handler, but asynchronous updates do not get the same treatment.
Consider the following scenario:

1. A button in ComponentA calls an action creator
2. The action creator calls an async API
3. As a result of the async call, the action creator dispatches an action
4. That action sets new state in a store
5. The new store data causes a new child to be mounted inside ComponentA
   (let's call it ComponentB)
6. ComponentB fires an action immediately, via componentDid/WillMount

Unlike re-renders from synchronous action dispatches (which generally
happen in the context of a synthetic event), asynchronous dispatches
aren't called within the context of React's batching strategy. This means
the dispatch loop is still in progress when ComponentB mounts, causing
a cascading dispatch exception.

`Flux#setBatchingFunction` allows you to pass a batching function to
wrap dispatches in; in most (all?) cases, you'll want to pass
`React.addons.batchedUpdates` in order to force all dispatches to happen
in the context of React's batched updates.

Fixes #92
  • Loading branch information
Brandon Tilley committed Apr 6, 2015
1 parent d35b604 commit 51f7651
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 0 deletions.
19 changes: 19 additions & 0 deletions lib/dispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ var _clone = require("lodash-node/modern/lang/clone"),
_findKey = require("lodash-node/modern/object/findKey"),
_uniq = require("lodash-node/modern/array/uniq");

var defaultBatchingFunction = function(callback) {
callback();
};

var Dispatcher = function(stores) {
this.stores = {};
this.currentDispatch = null;
this.currentActionType = null;
this.waitingToDispatch = [];
this.batchingFunction = defaultBatchingFunction;

for (var key in stores) {
if (stores.hasOwnProperty(key)) {
Expand All @@ -28,6 +33,12 @@ Dispatcher.prototype.addStore = function(name, store) {
};

Dispatcher.prototype.dispatch = function(action) {
this.batchingFunction(function() {
this._dispatch(action);
}.bind(this));
};

Dispatcher.prototype._dispatch = function(action) {
if (!action || !action.type) {
throw new Error("Can only dispatch actions with a 'type' property");
}
Expand Down Expand Up @@ -141,4 +152,12 @@ Dispatcher.prototype.waitForStores = function(store, stores, fn) {
dispatch.waitCallback = fn;
};

Dispatcher.prototype.setBatchingFunction = function(fn) {
if (fn) {
this.batchingFunction = fn;
} else {
this.batchingFunction = defaultBatchingFunction;
}
};

module.exports = Dispatcher;
4 changes: 4 additions & 0 deletions lib/flux.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,8 @@ Flux.prototype.addStores = function(stores) {
}
};

Flux.prototype.setBatchingFunction = function(fn) {
this.dispatcher.setBatchingFunction(fn);
};

module.exports = Flux;
139 changes: 139 additions & 0 deletions test/unit/test_batched_updates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
var Fluxxor = require("../../"),
jsdom = require("jsdom");

var chai = require("chai"),
expect = chai.expect;

var Store = Fluxxor.createStore({
actions: {
"ACTIVATE": "handleActivate",
"LOAD_INITIAL_VALUE": "handleLoadInitialValue"
},

initialize: function() {
this.activated = false;
this.value = null;
},

handleActivate: function() {
this.activated = true;
this.emit("change");
},

handleLoadInitialValue: function() {
this.value = "testing";
this.emit("change");
}
});

var actions = {
activate: function(callback) {
setTimeout(function() {
try {
this.dispatch("ACTIVATE");
callback();
} catch (ex) {
callback(ex);
}
}.bind(this));
},

loadInitialValue: function() {
this.dispatch("LOAD_INITIAL_VALUE");
}
};

describe("Batching updates", function() {
var React, TestUtils;
var flux, App, ComponentA, ComponentB;

beforeEach(function() {
global.window = jsdom.jsdom().createWindow("<html><body></body></html>");
global.document = window.document;
global.navigator = window.navigator;
React = require("react/addons");
TestUtils = React.addons.TestUtils;

flux = new Fluxxor.Flux({store: new Store()}, actions);

App = React.createClass({
mixins: [Fluxxor.FluxMixin(React), Fluxxor.StoreWatchMixin("store")],

getStateFromFlux: function() {
return {
activated: this.getFlux().store("store").activated
};
},

render: function() {
if (!this.state.activated) {
return React.createElement(ComponentA);
} else {
return React.createElement(ComponentB);
}
}
});

ComponentA = React.createClass({
mixins: [
Fluxxor.FluxMixin(React)
],

render: function() {
return React.DOM.div();
}
});

ComponentB = React.createClass({
mixins: [
Fluxxor.FluxMixin(React),
Fluxxor.StoreWatchMixin("store")
],

getStateFromFlux: function() {
return {
value: this.getFlux().store("store").value
};
},

componentWillMount: function() {
this.getFlux().actions.loadInitialValue();
},

render: function() {
return React.DOM.div();
},
});
});

afterEach(function() {
delete global.window;
delete global.document;
delete global.navigator;
for (var i in require.cache) {
if (require.cache.hasOwnProperty(i)) {
delete require.cache[i]; // ugh react why
}
}
});

it("doesn't batch by default", function(done) {
/* jshint expr:true */
TestUtils.renderIntoDocument(React.createElement(App, {flux: flux}));
flux.actions.activate(function(err) {
expect(err).to.match(/dispatch.*another action/);
done();
});
});

it("allows batching", function(done) {
/* jshint expr:true */
flux.setBatchingFunction(React.addons.batchedUpdates);

TestUtils.renderIntoDocument(React.createElement(App, {flux: flux}));
flux.actions.activate(function(err) {
expect(err).to.be.undefined;
done();
});
});
});

0 comments on commit 51f7651

Please sign in to comment.