Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should flux stores, or actions (or both) touch external services?

I've seen the flux pattern implemented both ways, and after having done both myself (initially going with the former approach), I believe that stores should be dumb recipients of data from the actions, and that asynchronous processing of writes should live in the action creators. (Async reads can be handled differently.) In my experience, this has a few benefits, in order of importance:

  1. Your stores become completely synchronous. This makes your store logic much easier to follow and very easy to test—just instantiate a store with some given state, send it an action, and check to see if the state changed as expected. Furthermore, one of the core concepts in flux is to prevent cascading dispatches and to prevent multiple dispatches at once; this is very difficult to do when your stores do asynchronous processing.

  2. All action dispatches happen from the action creators. If you handle asynchronous operations in your stores and you want to keep your stores' action handlers synchronous (and you should in order to get the flux single-dispatch guarantees), your stores will need to fire additional SUCCESS and FAIL actions in response to asynchronous processing. Putting these dispatches in the action creators instead helps separate the jobs of the action creators and the stores; furthermore, you don't have to go digging through your store logic to figure out where actions are being dispatched from. A typical asynchronous action in this case might look something like this (change the syntax of the dispatch calls based on the flavor of flux you're using):

    someActionCreator: function(userId) {
      // Dispatch an action now so that stores that want
      // to optimistically update their state can do so.
      dispatch("SOME_ACTION", {userId: userId});
    
      // This example uses promises, but you can use Node-style
      // callbacks or whatever you want for error handling.
      SomeDataAccessLayer.doSomething(userId)
      .then(function(newData) {
        // Stores that optimistically updated may not do anything
        // with a "SUCCESS" action, but you might e.g. stop showing
        // a loading indicator, etc.
        dispatch("SOME_ACTION_SUCCESS", {userId: userId, newData: newData});
      }, function(error) {
        // Stores can roll back by watching for the error case.
        dispatch("SOME_ACTION_FAIL", {userId: userId, error: error});
      });
    }
    

    Logic that may otherwise be duplicated across various actions should be extracted into a separate module; in this example, that module would be SomeDataAccessLayer, which handles doing the actual Ajax request.

  3. You need less action creators. This is less of a big deal, but nice to have. As mentioned in #2, if your stores have synchronous action dispatch handling (and they should), you'll need to fire extra actions to handle the results of asynchronous operations. Doing the dispatches in the action creators means that a single action creator can dispatch all three action types by handling the result of the asynchronous data access itself.


I tweeted this question to the devs at Facebook and the answer I got from Bill Fisher was:

When responding to a user's interaction with the UI, I would make the async call in the action creator methods.

But when you have a ticker or some other non-human driver, a call from the store works better.

The important thing is to create an action in the error/success callback so data always originates with actions


The stores should do everything, including fetching data, and signalling to components that the store's data has been updated. Why? Because actions can then be lightweight, disposable and replaceable without influencing important behavior. All important behavior and functionality happen in the store. This also prevents duplication of behavior that would otherwise be copied in two very similar but different actions. The stores are your single source of (handling the) truth.

In every Flux implementation I've seen Actions are basically event strings turned into objects, like traditionally you'd have an event named "anchor:clicked" but in Flux it would be defined as AnchorActions.Clicked. They're even so "dumb" that most implementations have separate Dispatcher objects to actually dispatch the events to the stores that are listening.

Personally I like Reflux' implementation of Flux where there are no separate Dispatcher objects and Action objects do the dispatching themselves.


edit: Facebook's Flux actually fetches in "action creators" so they do use smart actions. They do also prepare the payload using the stores:

https://github.com/facebook/flux/blob/19a24975462234ddc583ad740354e115c20b881d/examples/flux-chat/js/actions/ChatMessageActionCreators.js#L27 (line 27 and 28)

The callback on completion would then trigger a new action this time with the fetched data as payload:

https://github.com/facebook/flux/blob/19a24975462234ddc583ad740354e115c20b881d/examples/flux-chat/js/utils/ChatWebAPIUtils.js#L51

So I guess that's the better solution.


I'll provide an argument in favor of "dumb" Actions.

By placing the responsibility for collecting view data in your Actions, you couple your Actions to the data requirements of your views.

In contrast, generic Actions, that declaratively describe the intent of the user, or some state transition in your application, allows any Store that responds to that Action to transform the intent, into state tailored specifically for the views subscribed to it.

This lends itself to more numerous, but smaller, more specialized Stores. I argue for this style because

  • this gives you more flexibility in how views consume Store data
  • "smart" Stores, specialized for the views that consume them, will be smaller and less coupled for complex apps, than "smart" Actions, on which potentially many views depend

The purpose of a Store is to provide data to views. The name "Action" suggests to me that its purpose is to describe a change in my Application.

Suppose you have to add a widget to an existing Dashboard view, which shows some fancy new aggregate data your backend team just rolled out.

With "smart" Actions, you might need to change your "refresh-dashboard" Action, to consume the new API. However, "Refreshing the dashboard" in an abstract sense has not changed. The data requirements of your views is what has changed.

With "dumb" Actions, you might add a new Store for the new widget to consume, and set it up so that when it receives the "refresh-dashboard" Action type, it sends a request for the new data, and exposes it to the new widget once it's ready. It makes sense to me that when the view layer needs more or different data, the things that I change are the sources of that data: Stores.