Skip to content
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

Consider an async (promise) allowance for actions / stores #122

Open
mehcode opened this issue Apr 1, 2015 · 28 comments
Open

Consider an async (promise) allowance for actions / stores #122

mehcode opened this issue Apr 1, 2015 · 28 comments

Comments

@mehcode
Copy link

mehcode commented Apr 1, 2015

As ECMAScript 6 (and even 7) features become easier to achieve and use in a project, I feel that we should have allowances for using (what is now) first-class asynchronous primitives: Promises.

Stores may return promises in a handling method.

export default alt.createStore(class Store {
  // [...]
  onSend() {
    // This becomes much nicer when we can do "async onSend() [...]" in ES7
    return new Promise((resolve, reject) => { 
      resolve();
    });
  }
});

Actions return a promise by-default from the dispatch call.

export default alt.createActions(class Actions {
  // The actions generated from the shorthand methods become the following:
  send(params) {
    return this.dispatch(params)
  }
});

Internally, this.dispatch uses Promise.all to wait for all stores to finish execution and returns a chained promise that disposes of the resolved iterable.


The above allows for views that invoke actions to easily wait for a success or failure of a possibly asynchronous event.

Thoughts? I'll jump in and implement if we can agree that this is a good proposal for moving forward.

@goatslacker
Copy link
Owner

This breaks away from a few of the core principles of flux. Mainly, the actions then are not fire and forget and this would also mess with the central dispatcher.

I don't see how this can be implemented cleanly without stripping out the dispatcher.

You can already use ES7 async/await syntax with actions btw.

I am interested in hearing more about your use case though since we may be onto something here.

@mehcode
Copy link
Author

mehcode commented Apr 1, 2015

This breaks away from a few of the core principles of flux. Mainly, the actions then are not fire and forget and this would also mess with the central dispatcher.

I don't often have an action that I can forget about from a UX or even functional standpoint. If I understand correctly, Stores should be the only cog in the system that can or should interact with the data layer; meaning, the only place I would perform an AJAX request would be in a store.

If I send a session.create action I would like to know if it succeeded or failed. I even need to know why it failed (email/username not found, password invalid, etc.). I need to update the login form in a concise manner to describe the many different failures that can result. I need to know when to transition to the next route (that is being protected from the login form) on a successful session creation.

If I send a task.create action I would like to know if it succeeded or failed. Yes I can do optimistic rendering and pretend it always works in the view however if it fails, then what? Do we just silently remove the task from the store (which would be bad for UX) or do we loudly remove the task from the store by sending a notification.show action from the store if it fails (which I believe is also against the flux principles).

I don't see how this can be implemented cleanly without stripping out the dispatcher.

When I said I'd jump in and implement it, I meant I have it working locally with monkey patching and can clean it up and submit a PR.

You can already use ES7 async/await syntax [...]

I realize this (our codebase has this everywhere and its awesome) but I usually go for the principle of least astonishment when explaining a request. The average JS coder on github would have seen async .. and rejected the entire message because of its use.

[...] with actions

It wouldn't feel right (by my understanding of flux) for actions to be interacting with the REST API with AJAX requests (and that is the only reason I have to be async). That is the stores job (again my understanding could be wrong).

@troutowicz
Copy link
Contributor

Actions can totally handle async operations, and it actually makes more sense to have them there instead of the stores. Upon finishing an async operation, another action will need to be fired to pass the result to the dispatcher. Doing this in the action creator helps keep a clean separation of concerns.

@jareware
Copy link
Contributor

jareware commented Apr 1, 2015

I've been thinking of a way to achieve a similar end result, though I have to agree with @troutowicz that action creators seem the most natural place for something like this.

The method I had in mind (and actually briefly discussed with @goatslacker on Gitter) was as follows:

  • There's currently so semantics (AFAIK) attached to a return value of an action creator, so it's fair game for an extension point :)
  • Due to the reasoning before in this thread, it's also a natural place where to initiate async operations
  • It's very easy to listen to every dispatch the application makes (e.g. app.dispatcher.register(listener)) and look at its payload (i.e. action and arguments)
  • My proposal would be to allow that payload to (additionally) carry whatever the action creator returned (in a result property for example)

This would have several nice properties IMO, such as:

  • Being very unopinionated & extendable, as the rest of alt, so that you could also decide to, for example
    • return a simple boolean from the action creators to indicate whether or not that action succeeded
    • return an Rx Observable if Promises are not your thing
  • Being very unobtrusive: unless you look at the result property of the payload, you don't ever have to care such a mechanism even exists in the framework
  • Composes nicely at the action creators; consider:
doAction() {
  return http.get('/data')
    .then(this.actions.handleResult)
    .then(this.actions.subscribeToUpdates); // if this action also returns a Promise, we'll automatically wait for it as well
}

The most obvious use case for this would be isomorphic rendering, where it's not trivial to know when the application is ready to be sent down to the client (we've referred to this as the "Isomorphic Halting Problem" elsewhere :). This would allow you to do something along the lines of:

var app = new MyAltApp();
var ops = [];
var latest;

app.dispatcher.register(payload => {
  latest = payload;
  ops.push(payload.result);
  Promise.all(ops).then(() => {
    if (latest === payload) {
      // ready to send response!
    }
  });
});

app.actions.routing.route('/foo/bar');

Other isomorphic-capable Flux implementations (such as Fluxible) necessitate the "routing" action being the one that determines when things are ready. I feel this method would make it possible to wait on a more general level, regardless of where, how and how many actions are dispatched on the server-side.

What do you think?

@mehcode
Copy link
Author

mehcode commented Apr 1, 2015

Actions can totally handle async operations, and it actually makes more sense to have them there instead of the stores. Upon finishing an async operation, another action will need to be fired to pass the result to the dispatcher. Doing this in the action creator helps keep a clean separation of concerns.

Yes. Actions can handle asynchronous operations. I was under the impression that views do not listen to actions (as the flow should be uni-directional)? This means I'll have lots of "view state" in stores (meaning they are not very re-usable); the stores become very dumb (with all of the REST API interaction logic in the actions); and my actions become quite complex (going against what I've read elsewhere -- your actions file should be one, long file).


@jareware I agree. I'd be fine with exactly as you describe with the addition that this.dispatch returns a promise that is resolved / rejected when all dispatched stores are finished. Currently this.dispatch returns nothing and store callbacks return nothing or false.

return an Rx Observable if Promises are not your thing

I'm going to say that because alt pushes ES6 then it should push / require Promises (as they are a very ES6 concept). Using anything else for async operations at this point (with window.Promise being a thing even in IE [albeit the technical preview]) isn't maintainable.


@goatslacker The changes I'm asking for (in addition to what @jareware presents) would remove the necessity for store.emitChange. A promise could be returned that when resolved with nothing or false becomes the same behavior as a normal return (allowing the delay of change notifications).

@goatslacker
Copy link
Owner

I'd be interested in seeing some code :)

@mehcode
Copy link
Author

mehcode commented Apr 1, 2015

@goatslacker I'll get working on a PR then.

@jdlehman
Copy link
Collaborator

jdlehman commented Apr 2, 2015

I am interested in seeing some code as well, but wanted to weigh in my initial thoughts as well.

I agree with the initial sentiments of @troutowicz and @goatslacker. I think that the introduction of promises, especially in stores, goes against basic Flux principles, like actions being fire and forget. Stores should just reflect data at a certain point in time. As actions occur, the data may change, which is then signaled to the view via emitChange. I think there is additional complexity with having views wait on promises in the stores to resolve.

If I send a session.create action I would like to know if it succeeded or failed. I even need to know why it failed...

The typical pattern I have seen is to fire an action that updates the store to signify that the request has begun. At this point eager loading can be used as you mentioned, or a loader or something in the UI to represent that the request is loading. Upon completion of the request, we can look at the data and fire a fail action, or success action, which will again update our store data and ultimately our view to get it into its final state (until another action occurs). So essentially for async requests, we fire an action when we begin the request, and fire another (fail or success) when the request returns. This sticks with the pattern of firing actions and forgetting.

I remember that when I first started working for Flux, I initially wanted to wrap my actions in promises for some of the reasons you mentioned, but as I have worked with it more, I have found that I have not had need to do this (at least not yet). In async calls to other web services my app needs to communicate with, I simply call another action on the requests completion:

class MyActions {
  ...
  myAction(data) {
    // this dispatch will set data in the store that lets my views know
    // to display a loader/ optimistically render
    this.dispatch(data); 

    request.get(url)
      .query({name: category})
      .end((res) => {
        // fire fail or success action once request returns
        // this will handle getting our store/ui into the correct state
        if(res.ok) {
          this.actions.fireSuccess(res);
        }
        else {
          this.actions.fireFail(res);
        }
     });
  }
}

Ultimately, I do not see the need for Promises in Stores, but can see where they might seem useful in Actions. I still do not believe that Actions need to return promises as we can accomplish the same thing without them as shown above, but I definitely would like to see your PR because maybe a concrete example will help me better understand where you are coming from.

Though it is tempting, I think introducing promises in this manner goes against the Flux pattern, while alt maintains to stay true to vanilla Flux. I do want to make it clear that I am not completely against promises, I think they are a useful tool, but I do not see the need in the situation described.

I also wanted to cite some things I have read in the past from various individuals at Facebook that have shared why they have gone against the Promise approach at Facebook.

  • Bill Fisher (Facebook engineer) discusses that the most important aspect is not whether async requests are made in stores or actions, but that the success/error callback creates an action to maintain the unidirectional flow. That said he does push towards handing data requests in actions (or I guess as facebook calls them, action creators). He also talks about why promises are not used and the synchronous approach of the dispatcher is part of the design (there is some good discussion in that thread as well).
  • Ian Obermiller (Facebook engineer) discusses fire and forget actions (see point 3). He also mentions why they did not embrace promises.

@jareware
Copy link
Contributor

jareware commented Apr 2, 2015

Thoroughly agreed (esp. on why dispatch should be synchronous). Except:

I still do not believe that Actions need to return promises as we can accomplish the same thing without them as shown above

How would you know when the application's done retrieving its initial data when rendering server-side, though?

@goatslacker
Copy link
Owner

How would you know when the application's done retrieving its initial data when rendering server-side, though?

This is one case where being able to return promises from actions makes sense (if you keep your stores synchronous) you can aggregate the all the actions and then render when they all complete.

@jdlehman
Copy link
Collaborator

jdlehman commented Apr 2, 2015

Great point @jareware. I can get behind that! I just want to make sure we stick close to Flux, but it sounds like we are moving in the right direction. 👍

@goatslacker
Copy link
Owner

Any pseudo code you can share here. I'd love to take a look and start playing with it.

@ziflex
Copy link

ziflex commented Apr 13, 2015

I strongly believe that returning promises from actions is wrong and breaks Flux principles.
Agree with @jdlehman:

I remember that when I first started working for Flux, I initially wanted to wrap my actions in promises for some of the reasons you mentioned, but as I have worked with it more, I have found that I have not had need to do this (at least not yet).

I had the same thoughts, but then I realized that this limitation keeps my code cleaner and simpler.
I think it's much better to just add extra MyActionComplete and MyActionFail actions for handling operation's results.

How would you know when the application's done retrieving its initial data when rendering server-side, though?

@jareware there must be a better way to solve it :)

@jareware
Copy link
Contributor

@ziflex, with regard to:

I think it's much better to just add extra MyActionComplete and MyActionFail actions for handling operation's results.

Returning Promises from actions in no way precludes this, and this is in fact exactly what I do with alt.

The init/done/fail action triplet is still the one that changes the internal world state (as per Flux conventions), and the Promise is only there to let an external party know an action they dispatched (and any of its consequences such as a done/fail action) have been processed. It's just a more abstract (and therefore more general) way of answering that question than adding specific listeners for a bunch of actions you think will happen.

Does that make sense?

@mehcode
Copy link
Author

mehcode commented Apr 14, 2015

I've submitted #159 (pending review before I add tests) to ease promises with stores.


I'm struggling to think of the best way to have actions interact with stores. Currently you can just return a promise from an action and have it work. It's not being tracked anywhere but it allows then-able behavior.

Problems that I'm trying to resolve:

  • It feels obtuse to need to have actionFail and actionSuccess for each asynchronous action.
  • I would like an easy way to wait for everything to complete from the result of an action.

The action creator (method on the action object) itself invokes dispatch. This is what then invokes any registered listeners with the action.

Trying to wait for everything may be better accomplished by running your actions and then waiting for the stores to finish updating.


Just talking on paper here. If we can make action, actionFail, and actionSuccess more accessible I would be happy.

@jareware
Copy link
Contributor

Trying to wait for everything may be better accomplished by running your actions and then waiting for the stores to finish updating.

How do you know that from stores updating? Isn't it fairly common for action to also update the stores (e.g. with an "action in progress" flag)? Isn't it possible that triggering action will consequently trigger more than just actionSuccess? Consider an action that fetches a resource from the server, but then looking at the response may or may not fetch more resources. It would be hard to know how many "store updates" you have to wait for, whereas with Promise-able actions it would be as simple as:

doAction() {
  return http.get('/data')
    .then(this.actions.receiveData)
    .then(response => {
      if (response.needsMore) {
        return http.get('/more-data')
          .then(this.actions.receiveData)
      }
    })
}

Because the Promises are chained (notice we always return) you can be sure that whatever doAction() kicked off is completely done.

I would like an easy way to wait for everything to complete from the result of an action.

Indeed! The only change this would need is the ability to "collect" all Promises returned by action dispatches somehow (see above for my suggestion on this).

In this case you wouldn't actually even need to be careful to always return the Promise, as the dispatcher would be aware of new operations being kicked off.

It feels obtuse to need to have actionFail and actionSuccess for each asynchronous action.

Some Flux implementations choose to have automagic for this repetition, but given how simple it is to generateActions() in Alt I think this would go too far in hiding what's actually happening. That is, in my humble opinion at least. :)

@ziflex
Copy link

ziflex commented Apr 14, 2015

@jareware

The init/done/fail action triplet is still the one that changes the internal world state (as per Flux conventions), and the Promise is only there to let an external party know an action they dispatched (and any of its consequences such as a done/fail action) have been processed. It's just a more abstract (and therefore more general) way of answering that question than adding specific listeners for a bunch of actions you think will happen.

If you have some logic which might be called outside of your app or outside of your FLUX architecture, why not just move it outside of UI layer and make it more 'low-level' service which is not part of FLUX architecture ?

But I agree that how to sync actions is not completely clear and 'waitFor' isn't best solution.

Maybe it's better to ask FLUX creators what the best practices are ? :)

@jareware
Copy link
Contributor

That is of course a wholly valid option as well: to gather the Promises at whatever it is that generates them. The only reason why I'd imagine we're even discussing adding that to alt is that since it's such a common pattern to have actions fire off other actions async, it might make sense to add some sort of (optional) support for that to the framework itself. :)

@jcalfee
Copy link

jcalfee commented Jun 28, 2015

We are using IndexedDb, so everything is async in our Store layer. Is there a clear solution ideally one that does not involve another library? They way I understand it, it is important for the dispatcher to know when the store has completed its task as that is a sync point. I'm not sure how to do this outside of the framework.

Since we are adding lots of code to our project in this area so it would be nice to get it correct ASAP. Thank you for your help!

@goatslacker
Copy link
Owner

Why can't you persist when the dispatch is done and then handle success/failure the same way you'd handle success/fail if you were making an async call to fetch data.?

@jcalfee
Copy link

jcalfee commented Jun 29, 2015

Let me try and repeat your idea, I am probably unclear on the process...

So, my action object will call this.dispatch and alt will dispatch this to my store. At this point, the dispatcher is "done" as you mentioned above? So, the dispatcher moves on even though the event was handed off to indexeddb. Finally, when IndexedDb calls onsuccess then what? How is the view notified? How does the dispatcher know the store is done and ready for more? I may mis-understanding, but I thought the dispatcher was a sync point/ traffic controller: https://youtu.be/nYkdrAPrdcw?t=19m17s

Wouldn't the dispatcher need to know when indexedDb is done?

Thank you for your help! This really has to 'sink in' ..

@goatslacker
Copy link
Owner

A naive example

const DB = {
  save(text) {
    IndexedDB.saveSomewhere(text, function (err) {
      if (err) FooActions.shitAnError(err)
      else FooActions.done()
    })
  }
}

class FooActions {
  constructor() {
    this.generateActions('done', 'shitAnError')  
  }

  updateTodo(text) {
    DB.save(text)
    this.dispatch(text)
  }
}

class FooStore {
  constructor() {
    this.bindActions(FooActions)
    this.areWeDoneYet = true
    this.todoText = ''
  }

  updateTodo(text) {
    this.areWeDoneYet = false
    this.todoText = text

    // does not emit a change
    this.preventDefault()
  }

  // yay we're done saving...
  done() {
    this.areWeDoneYet = true
  }

  shitAnError(err) {
    // handle this maybe?
  }
}

You can just listen to this store like you would listen to any store normally.

@jcalfee
Copy link

jcalfee commented Jun 29, 2015

@goatslacker, your "naive example" really helps give me deeper insight into this.

I think I see an issue: DB depends on the actions calling it. That is a circular reference which will not even load. I think it is a requirement to notify all actions that have a dependency even if it were not the action to dispatch the call (please clarify).

  1. if DB could simply use a callback or a promise then the circular dependency would be gone and one might deploy (webpack, etc) DB without all the actions.
    OR
  2. FooActions(, etc) should listen as an EventListener for 'DB.done' and 'DB.shitAnError' while DB throws those errors..

@taion
Copy link
Collaborator

taion commented Jun 29, 2015

As mentioned above, why not just treat the db entirely as if it were a network remote? There's lots of patterns around dealing with async stuff happening there, and the concepts map quite closely. In other words, deal with IndexedDB exactly as you would deal with a web API, which you interact with in a similar async manner.

Since it's just async calls either way, everything should be almost exactly analogous.

@goatslacker
Copy link
Owner

The DB doesn't have to depend on an action to be called...I just assumed you would want it that way since the flow would be: action call -> update store with temporary data -> persist to db -> handle persist success or failure (ie: commit state or rollback state and inform user there was an error saving)

You could just call the DB directly.

The above code does have a circular dependency but you can easily refactor that out :)

const DB = {
  save(text) {
    // use promises too, whatever!
    return new Promise((resolve, reject) => {
      IndexedDB.saveSomewhere(text, function (err) {
        if (err) reject(err)
        else resolve()
      })
    })
  }
}

class FooActions {
  constructor() {
    this.generateActions('done', 'shitAnError')  
  }

  updateTodo(text) {
    DB.save(text).then(() => this.actions.done(), () => this.actions.shitAnError())
    this.dispatch(text)
  }
}

@khanetor
Copy link

@mehcode I also want something that you are talking about, for cases such as when I use a modal to create some new data, the modal should close upon success, or remain open upon error to save user from retyping. However, you do not need Promise in action to do this. You can use traditional callback.

In your action

class MyActions {
  someJutsu (arg1, arg2, callbackSuccess, callbackFailure) {
    asyncOperation(arg1, arg2).run({
      success: () => callbackSuccess(),
      error: () => callbackFailure()
    })
  }
}

In your view

class MyView extends Component {
  componentDidMount() {
    let success = () => showSuccessAlert();
    let error = () => showErrorAlert();
    MyActions.someJutsu("some arg1", "some arg2", success, error);
  }
  render () {...}
}

Hope this helps.

@wprater
Copy link

wprater commented Aug 6, 2015

a few of these examples are handled with the current data source implementation, no? is that still a recommended way to do async with actions?

@bradrydzewski
Copy link

We've run into similar dilemmas trying to determine how to handle async errors.

Would anyone consider modifying some of the examples to show the ideal way to handle errors? The weathertabs app, for example, might be a good place to demonstrate how the authors of the library recommend handling errors and displaying and error message on a component:

error: () => {
   this.actions.setLoading(false);
   // send error to store? promise as originally suggested in this issue?
}

It would also be interesting to see how to flush an error message. For example, let's say I'm using react-router and the a new route is loaded, at which point the error message no longer applies to the data on screen.

I realize this may sound like trivial request, but very few (if any) flux frameworks or sample programs demonstrate error handling and display. It seems to be a source of confusion for developers that are new to flux (myself and my teammates included) and some examples would go a long way :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests