After building a React Native app with Flux I noticed how nice it was to use that workflow once you got used to it. Suddenly it wasn’t difficult to pinpoint data and state issues!
I think I might have found a pattern that still reaps the benefits and has a lot less boilerplate. No dispatcher, no event listeners, just two methods. This is because we’re using reactivity to re-render instead of events. It also makes your Blaze templates really easy to unit test!
I’d really love to hear your opinions on this. Do you think it makes it easier to reason about the data flow or does it just add noise? In a small app you could even skip actions and call the store directly.
Some principals are:
- Templates never mutate data directly
- Templates gather data and trigger/call actions
- Data mutation happens in one place (Store/Domain object)
- Stores shouldn’t update other Stores, this happens in Actions
Edit I would not recommend this for React, it’s better to use flux instead! However for Blaze it still works nicely to keep everything predictable.
Ok now to the code. These are from this branch of the current React/Blaze Leaderboard app:
Actions are triggered in the templates:
Template.leaderboard.events({
'click .inc'() {
var playerId = Session.get("selectedPlayer");
PlayerActions.incrementScore(playerId, 5);
}
});
Actions call the stores directly to mutate the data. If this were a bigger app like Spotify, other stores would want to know about the 'pressedPlayButton' action.
PlayerActions = {
incrementScore: function(playerId, amount) {
PlayersDomain.handleIncrementScore(playerId, amount);
// FooDomain.handlePlayerIncrementScore(playerId, amount);
// BarDomain.handlePlayerIncrementScore(playerId, amount);
}
};
Finally the store can update the data. I wasn't sure if I should stick to Domain or keep the more React oriented 'Store' name. They're basically the same thing as Stores are groups of models within a domain. Thoughts?
PlayersDomain = {
// returns Number of docs updated
handleIncrementScore: function(docId, amount) {
return Players.update({_id: docId}, {$inc: {score: amount}});
},
...
};
Lastly templates can fetch data from the Domain with: `PlayersDomain.getAll()`
PlayersDomain = {
// returns Array of players document Objects
getAll() {
return Players.find({}, { sort: { score: -1, name: 1 } });
},
...
};
Full Domain example:
PlayersDomain = {
// returns Array of players document Objects
getAll: function() {
return Players.find({}, { sort: { score: -1, name: 1 } });
},
// returns doc id String
getSelectedPlayerId: function() {
return Session.get("selectedPlayer");
},
// returns full name String
getSelectedName: function() {
var selectedId = Session.get("selectedPlayer");
var player = Players.findOne({_id: selectedId});
return player && player.name;
},
// shouldn't be called directly from a template, only an action
handleSelectPlayer: function(id) {
Session.set("selectedPlayer", id);
},
// shouldn't be called directly from a template, only an action
// returns number of docs updated
handleIncrementScore: function(docId, amount) {
return Players.update({_id: docId}, {$inc: {score: amount}});
}
};