-
Notifications
You must be signed in to change notification settings - Fork 328
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Child (async) actions and promise handling #140
Conversation
I like this approach. It's essentially exactly the same as I have been doing already, see: |
👍 nice idea To be honest array as parameter in |
@spoike interesting, what would the interface look like using |
@willembult This is a side note and not in scope for this PR. However what I mean is this is currently: Reflux.createActions(['action1', 'action2']); How it really should've be done, without using array, just have a "varlist" instead: Reflux.createActions('action1', 'action2'); Implementation inside Reflux wouldn't change a lot, just use the whole |
Right, that makes sense. I guess what I meant to ask was, what would that look like if we want to pass options to actions (like in this PR)? |
I think having an options object is fine as this PR has it, since each key represents an action. @WRiddler is the semantics between child and parent actions any way bad idea? I think it's an okay concession for ajax-y actions and completely optional. The only thing missing in this PR is an update on README file. I'll review the code changes more closely when I get back home. What I've seen so far looks OK, as long as the promise helper avoids to pull in any particular 3rd party library. Should we add a test with alternative libraries such as bluebird? |
@spoike No, it's not a bad idea at all imo. Prevents some boilerplate. I'm in favor of the common-case helpers as well. |
…e existing sync option
This was confusing, but legit: Reflux.createActions({
'load': {async: true, sync: true}
}); So I renamed the option to |
This seems like a really cool idea for how to handle server communication and async code in general. +1 from me - looking forward to being able to use this! |
+1 on these. Sugar like this will keep this project on top! Thanks for the hard work! |
👍 |
@willembult will you write something about it in the README or would you like someone else to do it? |
Sure, I'll do it. Pretty slammed right now, but I'll look at it this weekend. — On Fri, Dec 5, 2014 at 12:09 AM, Mikael Brassman notifications@github.com
|
|
||
var canHandlePromise = | ||
this.children.indexOf('completed') >= 0 && | ||
this.children.indexOf('failed') >= 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably better to check completed
and failed
are last two elements in this.children
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about this then?
createActions({
myAction: {'children': ['completed','failed','progressed']}
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't that be concatenated with ['completed','failed']
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only if you use the "asyncResult" option. So you could use this:
createActions({
myAction: {'children': ['progressed'], asyncResult: true}
});
and it would append ['completed','failed']
because of asyncResult
.
But you could also do this:
createActions({
myAction: {'children': ['completed','failed','progressed']}
});
in which case it can still handle promises, but since we've explicitly defined the child actions, they don't appear last in the list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see. I misunderstood.
Would a shorthand be a possibility? var ImageActions = Reflux.createActions({
'pictureUpload': {children: ['progressed','completed']}
});
// above and below are equivalent
var ImageActions = Reflux.createActions({
'pictureUpload': ['progressed','completed']
}); |
I don't understand
This increases API surface quite a lot providing no benefits. |
I don't like this Also, I think Then have: // callback as param
ProductActions.load.listen(function(cb) {
ProductAPI.load()
.then(cb.bind(this, null))
.catch(cb);
});
// or just return a promise
ProductActions.load.listen(function() {
return ProductAPI.load();
}); gulp does this already for quite a while: |
To elaborate, I'm thinking of this as an alternative: var ProductActions = Reflux.createActions({
'load': {asyncResult: true, callback: 'completed', children: [...]}
});
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(){
},
onLoadCompleted: function(err, result){
}
});
|
Sure, you could still use listen. listenAndPromise is a completely optional helper method that reduces a small amount of boilerplate.
Can you elaborate on why you'd prefer this? It's slightly more conventional and less explicit, and seems to me like it wouldn't reduce any LoC, because now we'll need to do a conditional inside the onLoadCompleted. In my mind, the data flowing from a completed load and a failed load are inherently different, so we should treat them as separate data flows and thus have separate handlers. |
Fair enough, I guess if you're not using any of the helper functions, then you'll have similar LoC. However, when you do use those, this: var ProductActions = Reflux.createActions([
'load',
'loadCompleted',
'loadFailed'
]);
ProductActions.load.preEmit = function () {
// Perform the API call, get a promise
ProductAPI.load()
.then(ProductActions.loadComplete)
.catch(ProductActions.loadFailed);
}; gets reduced to this var ProductActions = Reflux.createActions({
'load': {asyncResult: true}
});
ProductActions.load.listenAndPromise( ProductAPI.load ); The "semantic link", for lack of a better term, allows us precisely to provide those helper methods / options. Without having child actions, those helper methods would depend on other actions outside of their scope, which would be super messy. Async actions are super commonplace, so I think it's important to have easy facilities for them. Some even incorrectly reach the conclusion that Reflux doesn't cater well to async functionality, because we currently lack such explicit patterns. |
54a1584
to
34e734a
Compare
34e734a
to
30ecf12
Compare
👍 Really excited for this to get deployed in the v0.2.2 release! |
Is there any way for the child actions to access the arguments that the parent action was called with? |
Child (async) actions and promise handling
Congrats to everyone, particularly @willembult & @spoike, for this update! I'm modifying a large codebase with this support now. Can someone post a concise example on how this affects stores? Currently, I've been using this helper: Reflux.StoreMethods.promise = function(action) {
var args = Array.prototype.slice.call(arguments, 1);
return new Promise(function(resolve) {
this.listen(resolve);
action(args);
}.bind(this));
}; // MyActions.js
Reflux.createActions(['fetch', 'fetchSuccess', 'fetchError']); // MySore.js
Reflux.createStore({
onFetch: function(query) {
MyApi.search(query, function(response) {
return response.ok ? MyActions.fetchSuccess(response.body) : MyActions.fetchError(response.error);
});
}
}); And consume it via: MyStore.promise(MyActions.fetch, { id: 123 }).then(function(results) {
console.log('Promised results:', results);
}); So, naturally, I just got the error Thanks! |
Felt the need to comment here if anyone else runs across this as I've spent the last hour debugging a non-issue with this new Reflux feature: When using the 6to5 compiler with ES6,
Though the former form works perfectly:
Feel free to delete this comment if totally out of place! |
@damassi This is only due to the lexical binding in ES6. |
Thanks @jsdir -- I wasn't aware that this was going to be implemented, thinking instead that it was a bug in the compiler and that |
Async actions
There has been some discussion on asynchronous actions (#57, #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 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:
Issues with preEmit
While this is definitely the cleanest way I've seen, some things still bothered me about it:
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:
Common-case helpers
I figured it would make sense as well to provide some helper methods and options to make the 80% case easier. This works as such:
Alternative uses
For custom cases, you could create alternative children events:
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!