spoike/refluxjs — pull request #140 by willembult (2014-11-26 09:02:26+00:00)
Title: Child (async) actions and promise handling

# Async actions
There has been some discussion on asynchronous actions (https://github.com/spoike/refluxjs/issues/57, https://github.com/spoike/refluxjs/pull/103) and where to best handle them.

The consensus seems to be that triggered **execution is best kept outside of the stores**, and I personally agree. In my opinion, the store shouldn't worry about how actions are run, focusing merely on their results. It also makes testing easier, as a few folks have noted.

The pattern proposed in the [update on v0.1.8](http://spoike.ghost.io/refluxjs-0-1-8/) links the execution of asynchronous code to the preEmit hook of an action, after which it triggers events on "Completed" and "Failed" actions, like so:

```javascript
var ProductAPI = require('./ProductAPI');
// contains `load` method that returns a promise

var ProductActions = Reflux.createActions({
'load', // initiates the async load
'loadComplete', // when the load is complete
'loadError' // when the load has failed
});

// Hook up the API call in the load action's preEmit
ProductActions.load.preEmit = function () {
// Perform the API call, get a promise
ProductAPI.load()
.then(ProductActions.loadComplete)
.catch(ProductActions.loadError);
};
```

## Issues with preEmit
While this is definitely the cleanest way I've seen, some things still bothered me about it:

* There is **no semantic link** between the "load" and "loadComplete" / "loadError" events. Nothing in the interface reflects the fact that they are related.

* Putting an API call, or some other asynchronous code, in the **preEmit hook seems hacky** to me. Others have expressed similar sentiments (https://github.com/spoike/refluxjs/issues/57).

* I think it's **overly verbose**. There's still some unnecessary boilerplate here, and for every async event (of which there can be loads) we have to now manually define 3 separate actions. If at some point we want to add a "Canceled" action as well for all our calls, we'll be doing busy-work for a while.

## Proposal: Child actions
It appeared to me that if we could define child actions (attached to a parent action), we could design a much nicer interface. This commit is an implementation of that idea. The general interface looks as such:

```javascript
// ProductActions.js

var ProductAPI = require('./ProductAPI');
// contains `load` method that returns a promise

// Define action with child actions
var ProductActions = Reflux.createActions({
'load': {children: ['completed','failed']}
});

// And here is how you would hook it up to a promise
ProductActions.load.listen( function() {
// Bind listener context to action by default
ProductAPI.load()
.then(this.completed)
.catch(this.failed);
});
````
```javascript
// ProductStore.js

var ProductActions = require('./ProductActions');

// This is how you would listen for a completed load
ProductActions.load.completed.listen(function(result) {
// do something with result
});

// listenToMany (and ListenerMixin) will still work intuitively
var Store = Reflux.createStore({
init: function() {
this.listenToMany(ProductActions);
},
onLoad: function(){
// product load started
},
onLoadCompleted: function(product){
// product load completed
}
});
````

## Common-case helpers
I figured it would make sense as well to provide some helper methods and functions to make the 80% case easier. This works as such:

````javascript
// Define actions with async option to auto-create
// 'completed' and 'failed' child events
var ProductActions = Reflux.createActions({
'load': {async: true}
});

// Hook up standard async child functors
// to a promise easily:
ProductActions.load.listen( function() {
// Have the action attach callbacks to the promise
// and pass on data to "complete" and "failed" actions
ProductActions.load.promise(ProductAPI.load());
});

// Or even easier for the 80% case, a method that
// hooks ups callbacks to a returned promise directly
ProductActions.load.listenAndPromise( function() {
return ProductAPI.load();
});
````

## Alternative uses
For custom cases, you could create alternative children events:
````javascript
var ImageAPI = require('./ImageAPI');

// If we need custom child actions, we could do so:
var ImageActions = Reflux.createActions({
'pictureUpload': {children: ['progressed','completed']}
});

// Hook things up to a non-promise-based API
ImageActions.pictureUpload.listen(function(){
ImageAPI.upload({
onProgress: this.progressed,
onSuccess: this.completed
});
});
```

## Notes
Note, while createActions now operates on an associative Object, the interface remains backwards-compatible and accepts Arrays as well.

This has cleaned up my action definitions quite a bit. Curious to hear your critical thoughts / feedback! Thanks!