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

6

124

Should the stores maintain their own state and have the ability to call network and data storage services in doing so ...in which case the actions are just dumb message passers,

-OR-

...should the stores be dumb recipients of immutable data from the actions (and the actions be the ones that fetch/send data between external sources? Store in this instance would act as view-models and would be able to aggregate / filter their data prior to setting their own state base on the immutable data they were fed by the action.

It seems to me that it should be one or the other (rather than a mix of both). If so, why is one preferred / recommended over the other?

Awesome answered 2/9, 2014 at 19:10 Comment(2)
This post might help code-experience.com/…Kowalczyk
For those evaluating the various implementations of the flux pattern I'd highly recommend taking a look at Redux github.com/rackt/redux Stores are implemented as pure functions that take in the current state and emit a new version of that state. Since they're pure functions the question of whether or not they can call network and storage services is taken out of your hands: they can't.Awesome
A
151

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.

Any answered 3/9, 2014 at 15:54 Comment(4)
I think what originates the web api call (action creator vs. store) is less important than the fact that the success/error callback should create an action. So the data flow is then always: action -> dispatcher -> stores -> views.Durkee
Would putting the actual request logic within an API module be better/easier to test? So your API module could just return a promise that you dispatch from. The action creator just dispatches based on resolve/fail after sending out an initial 'pending' action. The question that remains is how the component listens to these 'events' as I'm not sure that the request state should map to store state.Erotomania
@Erotomania That's exactly what I do in the example above: dispatch an initial pending action ("SOME_ACTION"), use an API to make a request (SomeDataAccessLayer.doSomething(userId)) which returns a promise, and in the two .then functions, dispatch additional actions. Request state can (more or less) map to store state if the application needs to know about the status of the state. How this maps is up to the app (e.g. maybe each comment has an individual error state, a la Facebook, or maybe there's one global error component)Any
@MichelleTilley "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." That's a key point for me. Well said.Kev
K
51

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

Kowalczyk answered 4/9, 2014 at 4:15 Comment(2)
While this makes sense, any idea why a call from store works better when action triggers from non-human driver ?Marcusmarcy
@Marcusmarcy I guess if you have a live-ticker or something similar, you don't really need to fire an action and when you do that from the store, you probably have to write fewer code, since the store can instantly access the state & emit a change.Illdisposed
F
8

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.

Forcible answered 2/9, 2014 at 22:33 Comment(5)
What is this Reflux implementation? I haven't heard of it. Your answer is interesting. You mean that your store implementation should have the logic to do API calls and so on? I thought the stores should just receive data and just update their values. They filter on specific actions, and update some attributes of their stores.Cannady
Reflux is a slight variation of Facebook's Flux: github.com/spoike/refluxjs Stores manage the whole "Model" domain of your application, vs Actions/Dispatchers that only stitch & glue things together.Forcible
So I've been thinking about this some more and have (almost) answered my own question. I would have added it as an answer here (for others to vote on) but apparently I'm too karma-poor at stackoverflow to be able to post an answer yet. So here's a link: groups.google.com/d/msg/reactjs/PpsvVPvhBbc/BZoG-bFeOwoJAwesome
Thanks for the google group link, it seems really informative. I am also more fan of everything going through the dispatcher, and a really simple logic in the store, basically, updating their data that's all. @Rygu I will check reflux.Cannady
I edited my answer with an alternate view. Seems both solutions are possible. I would almost certainly pick Facebook's solution over others.Forcible
W
3

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.

Watercolor answered 11/10, 2015 at 5:15 Comment(0)
C
2

gaeron's flux-react-router-demo has a nice utility variation of the 'correct' approach.

An ActionCreator generates a promise from an external API service, and then passes the promise and three action constants to a dispatchAsync function in a proxy/extended Dispatcher. dispatchAsync will always dispatch the first action e.g. 'GET_EXTERNAL_DATA' and once the promise returns it will dispatch either 'GET_EXTERNAL_DATA_SUCCESS' or 'GET_EXTERNAL_DATA_ERROR'.

Chester answered 5/6, 2015 at 5:35 Comment(0)
F
1

If you want one day to have a development environment comparable to what you see in Bret Victor's famous video Inventing on Principle, you should rather use dumb stores that are just a projection of actions/events inside a data structure, without any side effect. It would also help if your stores were actually member of the same global immutable data structure, like in Redux.

More explainations here: https://mcmap.net/q/88292/-tools-to-support-live-coding-as-in-bret-victor-39-s-quot-inventing-on-principle-quot-talk

Frentz answered 22/7, 2015 at 9:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.